diff --git a/app/(main)/profile/attendance/page.jsx b/app/(main)/profile/attendance/page.jsx
index 3ef9e6e..0bb0004 100644
--- a/app/(main)/profile/attendance/page.jsx
+++ b/app/(main)/profile/attendance/page.jsx
@@ -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
@@ -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() {
@@ -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 ;
}
diff --git a/app/(main)/profile/page.jsx b/app/(main)/profile/page.jsx
index 625771f..ecd36d4 100644
--- a/app/(main)/profile/page.jsx
+++ b/app/(main)/profile/page.jsx
@@ -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 ;
}
diff --git a/app/api/form/attendance-events/route.ts b/app/api/form/attendance-events/route.ts
index 9d67a1f..13981a4 100644
--- a/app/api/form/attendance-events/route.ts
+++ b/app/api/form/attendance-events/route.ts
@@ -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
@@ -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({
diff --git a/app/api/form/attendance-stats/[id]/route.ts b/app/api/form/attendance-stats/[id]/route.ts
index 70df564..4780be4 100644
--- a/app/api/form/attendance-stats/[id]/route.ts
+++ b/app/api/form/attendance-stats/[id]/route.ts
@@ -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
@@ -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);
diff --git a/app/api/form/export-attendance/[id]/route.ts b/app/api/form/export-attendance/[id]/route.ts
index 3e2d4dc..09d9f7c 100644
--- a/app/api/form/export-attendance/[id]/route.ts
+++ b/app/api/form/export-attendance/[id]/route.ts
@@ -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(
@@ -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);
diff --git a/app/api/form/markAttendance/route.ts b/app/api/form/markAttendance/route.ts
index c13e7d7..482e796 100644
--- a/app/api/form/markAttendance/route.ts
+++ b/app/api/form/markAttendance/route.ts
@@ -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
@@ -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 });
diff --git a/lib/auth/attendance.ts b/lib/auth/attendance.ts
new file mode 100644
index 0000000..e282fd5
--- /dev/null
+++ b/lib/auth/attendance.ts
@@ -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();
+}
diff --git a/lib/auth/permissions.ts b/lib/auth/permissions.ts
index 3b36219..0afdbcd 100644
--- a/lib/auth/permissions.ts
+++ b/lib/auth/permissions.ts
@@ -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";
@@ -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 | null | undefined,
+): boolean {
+ if (!user) return false;
+ return isAdmin(user) || isAttendanceScanner(user);
+}
diff --git a/src/layouts/Profile/Sidebar/Sidebar.jsx b/src/layouts/Profile/Sidebar/Sidebar.jsx
index d916339..904db3f 100644
--- a/src/layouts/Profile/Sidebar/Sidebar.jsx
+++ b/src/layouts/Profile/Sidebar/Sidebar.jsx
@@ -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";
@@ -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");
@@ -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 (