Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions app/(main)/profile/attendance/page.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Route entry — renders the component ported from
// FED-Frontend/src/pages/AttendancePage/AttendancePage.jsx
//
// Gated to ADMIN on the server. The sidebar only ever *showed* this link to
// admins, but that is presentation: `proxy.ts` guards /profile by checking for
// Gated to admins and the door-duty scanner on the server. The sidebar only
// ever *showed* this link to those users, but that is presentation: `proxy.ts` guards /profile by checking for
// a valid session, not a role, so before this check any signed-in participant
// who typed the path got a working scanner. Combined with the fact that a
// participant can generate their own QR (that is the whole point of
Expand All @@ -13,7 +13,8 @@

import { redirect } from "next/navigation";

import { getCurrentUser, isAdmin } from "@/lib/auth/access";
import { getCurrentUser } from "@/lib/auth/access";
import { canMarkAttendance } from "@/lib/auth/permissions";
import AttendancePage from "@/src/views/AttendancePage/AttendancePage";

export default async function Page() {
Expand All @@ -22,7 +23,7 @@ export default async function Page() {
// Matches what proxy.ts does for an anonymous request to a protected route,
// so an expired session lands on the login page rather than a bare redirect.
if (!user) redirect("/Login?next=/profile/attendance");
if (!isAdmin(user)) redirect("/profile");
if (!canMarkAttendance(user)) redirect("/profile");

return <AttendancePage />;
}
10 changes: 8 additions & 2 deletions app/(main)/profile/page.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// Route entry — renders the component ported from
// FED-Frontend/src/sections/Profile/General/ProfileView/ProfileView.jsx
"use client";

import { redirect } from "next/navigation";

import { getCurrentUser } from "@/lib/auth/access";
import { isAttendanceScanner } from "@/lib/auth/attendance";
import ProfileView from "@/src/sections/Profile/General/ProfileView/ProfileView";

export default function Page() {
export default async function Page() {
const user = await getCurrentUser();
if (user && isAttendanceScanner(user)) redirect("/profile/attendance");

return <ProfileView />;
}
5 changes: 3 additions & 2 deletions app/api/form/attendance-events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
listAttendanceEvents,
} from "@/lib/services/attendance";
import { expressError, handle, json } from "@/lib/api/express";
import { getCurrentUser, isAdmin } from "@/lib/auth/access";
import { getCurrentUser } from "@/lib/auth/access";
import { canMarkAttendance } from "@/lib/auth/permissions";

/**
* GET /api/form/attendance-events
Expand All @@ -15,7 +16,7 @@ export async function GET() {
return handle(async () => {
const user = await getCurrentUser();
if (!user) return expressError(401, "Token is required");
if (!isAdmin(user)) return expressError(403, "Unauthorized");
if (!canMarkAttendance(user)) return expressError(403, "Unauthorized");

const events = await listAttendanceEvents();
return json({
Expand Down
5 changes: 3 additions & 2 deletions app/api/form/attendance-stats/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getAttendanceStats } from "@/lib/services/attendance";
import { expressError, handle, json } from "@/lib/api/express";
import { getCurrentUser, isAdmin } from "@/lib/auth/access";
import { getCurrentUser } from "@/lib/auth/access";
import { canMarkAttendance } from "@/lib/auth/permissions";

/**
* GET /api/form/attendance-stats/:id
Expand All @@ -14,7 +15,7 @@ export async function GET(
return handle(async () => {
const user = await getCurrentUser();
if (!user) return expressError(401, "Token is required");
if (!isAdmin(user)) return expressError(403, "Unauthorized");
if (!canMarkAttendance(user)) return expressError(403, "Unauthorized");

const { id } = await ctx.params;
const stats = await getAttendanceStats(id);
Expand Down
9 changes: 5 additions & 4 deletions app/api/form/export-attendance/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { exportAttendance } from "@/lib/services/attendance";
import { expressError, handle } from "@/lib/api/express";
import { getCurrentUser, isAdmin } from "@/lib/auth/access";
import { getCurrentUser } from "@/lib/auth/access";
import { canMarkAttendance } from "@/lib/auth/permissions";

/**
* GET /api/form/export-attendance/:id
* Port of controllers/registration/exportAttendance.
*
* Admin only, matching the route's `checkAccess("ADMIN")`. This previously
* accepted any club member, which handed the full attendee list of any event to
* Admin and door-duty scanner, matching the route's `checkAccess("ADMIN")` plus
* the dedicated attendance account. This previously accepted any club member, which handed the full attendee list of any event to
* every executive rather than to admins alone.
*/
export async function GET(
Expand All @@ -17,7 +18,7 @@ export async function GET(
return handle(async () => {
const user = await getCurrentUser();
if (!user) return expressError(401, "Token is required");
if (!isAdmin(user)) return expressError(403, "Unauthorized");
if (!canMarkAttendance(user)) return expressError(403, "Unauthorized");

const { id } = await ctx.params;
const { filename, csv } = await exportAttendance(id);
Expand Down
7 changes: 4 additions & 3 deletions app/api/form/markAttendance/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { markAttendance } from "@/lib/services/attendance";
import { body, expressError, handle, json } from "@/lib/api/express";
import { getCurrentUser, isAdmin } from "@/lib/auth/access";
import { getCurrentUser } from "@/lib/auth/access";
import { canMarkAttendance } from "@/lib/auth/permissions";

/**
* POST /api/form/markAttendance
* Port of controllers/registration/markAttendance.js.
*
* ADMIN only. This deliberately diverges from the Express route, which has its
* ADMIN and the dedicated door-duty account. This deliberately diverges from the Express route, which has its
* `checkAccess` commented out entirely and so accepts unauthenticated calls.
*
* The QR token alone is not an access control: a participant can mint their own
Expand All @@ -26,7 +27,7 @@ export async function POST(request: Request) {
return handle(async () => {
const user = await getCurrentUser();
if (!user) return expressError(401, "Token is required");
if (!isAdmin(user)) return expressError(403, "Unauthorized");
if (!canMarkAttendance(user)) return expressError(403, "Unauthorized");

const b = await body<{ formId?: string; token?: string }>(request);
const result = await markAttendance({ formId: b.formId, token: b.token });
Expand Down
9 changes: 9 additions & 0 deletions lib/auth/attendance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** Dedicated door-duty account — sidebar shows only the attendance scanner. */
export const ATTENDANCE_SCANNER_EMAIL = "attendance@fedkiit.com";

export function isAttendanceScanner(
user: Pick<{ email: string }, "email"> | null | undefined,
): boolean {
if (!user) return false;
return user.email.toLowerCase() === ATTENDANCE_SCANNER_EMAIL.toLowerCase();
}
16 changes: 16 additions & 0 deletions lib/auth/permissions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import "server-only";

import type { SafeUser } from "@/lib/auth/access";
import { isAdmin } from "@/lib/auth/access";
import { isAttendanceScanner } from "@/lib/auth/attendance";
import { FORM_ANALYTICS_ROLES } from "@/lib/auth/roles";
import { envList, getEnv } from "@/lib/env";

Expand Down Expand Up @@ -57,3 +59,17 @@ export function canViewFormAnalytics(
);
return allowed.includes(user.email.toLowerCase());
}

/**
* Who may open the attendance scanner and call its APIs.
*
* ADMIN plus the dedicated door-duty account. Both the page and every
* attendance endpoint use this so the sidebar cannot promise access the
* server will refuse.
*/
export function canMarkAttendance(
user: Pick<SafeUser, "access" | "email"> | null | undefined,
): boolean {
if (!user) return false;
return isAdmin(user) || isAttendanceScanner(user);
}
5 changes: 3 additions & 2 deletions src/layouts/Profile/Sidebar/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SiReacthookform } from "react-icons/si";
import { FaRegNewspaper, FaCertificate, FaChevronDown } from "react-icons/fa";
import { LuClipboardList } from "react-icons/lu";
import AuthContext from "../../../context/AuthContext";
import { isAttendanceScanner } from "@/lib/auth/attendance";
import styles from "./styles/Sidebar.module.scss";

import defaultImg from "../../../assets/images/defaultImg.jpg";
Expand All @@ -33,7 +34,7 @@ const Sidebar = ({ activepage, handleChange }) => {
const access = authCtx.user.access;
const email = authCtx.user.email; // Assuming email is available in authCtx.user

if (email === "attendance@fedkiit.com") {
if (isAttendanceScanner({ email })) {
setDesignation("Attendance Only");
} else if (access === "ADMIN") {
setDesignation("Admin");
Expand Down Expand Up @@ -92,7 +93,7 @@ const Sidebar = ({ activepage, handleChange }) => {
};

// Check if user is attendance-only
const isAttendanceOnly = authCtx.user.email === "attendance@fedkiit.com";
const isAttendanceOnly = isAttendanceScanner(authCtx.user);

const renderBlogMenu = () => {
return (
Expand Down
Loading