diff --git a/prisma/migrations/20260318174510_is_developer/migration.sql b/prisma/migrations/20260318174510_is_developer/migration.sql new file mode 100644 index 0000000..836a45d --- /dev/null +++ b/prisma/migrations/20260318174510_is_developer/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "app"."staff_user" ADD COLUMN "is_developer" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/models/app/staff_user.prisma b/prisma/models/app/staff_user.prisma index 387820d..0158b36 100644 --- a/prisma/models/app/staff_user.prisma +++ b/prisma/models/app/staff_user.prisma @@ -22,6 +22,7 @@ model StaffUser { isPesticideDesignate Boolean @default(false) @map("is_pesticide_designate") isFinanceDesignate Boolean @default(false) @map("is_finance_designate") isIta2Designate Boolean @default(false) @map("is_ita2_designate") + isDeveloper Boolean @default(false) @map("is_developer") @@map("staff_user") @@schema("app") diff --git a/src/app/protected/settings/developer/page.tsx b/src/app/protected/settings/developer/page.tsx new file mode 100644 index 0000000..fbaf801 --- /dev/null +++ b/src/app/protected/settings/developer/page.tsx @@ -0,0 +1,43 @@ +import { revalidatePath } from "next/cache" +import { headers } from "next/headers" +import { RoleSwitcher } from "@/components/settings/developer/RoleSwitcher" +import { getStaffUserBySub } from "@/lib/prisma/staff_user/getStaffUserBySub" +import { updateStaffUser } from "@/lib/prisma/staff_user/updateStaffUser" +import { getAuthContext } from "@/utils/auth/getAuthContext" + +// This page should always be rendered dynamically to ensure fresh data +export const dynamic = "force-dynamic" + +const revalidateDeveloperPage = async () => { + "use server" + revalidatePath("/protected/settings/developer") +} + +export default async function Page() { + const headersList = await headers() + const authContext = getAuthContext(headersList) + const user = authContext?.user + + const currentUser = await getStaffUserBySub(user?.sub ?? "") + + if (!currentUser?.isDeveloper) { + return ( +
+

You do not have access to this page.

+
+ ) + } + + return ( +
+

Developer Settings

+
+ +
+
+ ) +} diff --git a/src/app/protected/settings/page.tsx b/src/app/protected/settings/page.tsx index 890f676..0b47aa5 100644 --- a/src/app/protected/settings/page.tsx +++ b/src/app/protected/settings/page.tsx @@ -1,12 +1,35 @@ +import { ShieldCheckIcon } from "@heroicons/react/24/outline" +import { headers } from "next/headers" import { NAV_ITEMS } from "@/components/settings/NAV_ITEMS" import { NavItem } from "@/components/settings/NavItem" +import { getStaffUserBySub } from "@/lib/prisma/staff_user/getStaffUserBySub" +import { getAuthContext } from "@/utils/auth/getAuthContext" export default async function Page() { + const headersList = await headers() + const authContext = getAuthContext(headersList) + const user = authContext?.user + + const currentUser = await getStaffUserBySub(user?.sub ?? "") + + const navigationItems = [ + ...NAV_ITEMS, + ...(currentUser?.isDeveloper + ? [ + { + label: "Developer Settings", + href: "/protected/settings/developer", + icon: , + }, + ] + : []), + ] + return (

Settings

- {NAV_ITEMS.map((item) => ( + {navigationItems.map((item) => ( ))}
diff --git a/src/components/settings/developer/RoleSwitcher.tsx b/src/components/settings/developer/RoleSwitcher.tsx new file mode 100644 index 0000000..f30d61f --- /dev/null +++ b/src/components/settings/developer/RoleSwitcher.tsx @@ -0,0 +1,66 @@ +"use client" + +import { useState } from "react" +import { SelectInput } from "@/components/common/select" +import type { Role, StaffUser } from "@/generated/prisma/client" + +type RoleSwitcherProps = { + currentUser: StaffUser + updateStaffUser: ( + user: Partial, + prevUser: Partial, + availableRoles?: Role[] + ) => Promise + revalidatePage: () => Promise +} + +const AVAILABLE_ROLES: Role[] = ["Authenticated", "CSR", "SCSR", "SDM", "Administrator"] + +export const RoleSwitcher = ({ + currentUser, + updateStaffUser, + revalidatePage, +}: RoleSwitcherProps) => { + const [isUpdating, setIsUpdating] = useState(false) + const [error, setError] = useState(null) + + const handleRoleChange = async (newRole: Role) => { + if (newRole === currentUser.role) return + + try { + setIsUpdating(true) + setError(null) + await updateStaffUser({ ...currentUser, role: newRole }, currentUser, AVAILABLE_ROLES) + await revalidatePage() + setIsUpdating(false) + window.location.href = "/protected/settings/developer" + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to update role") + setIsUpdating(false) + } + } + + return ( +
+
+ handleRoleChange(value as Role)} + disabled={isUpdating} + options={AVAILABLE_ROLES.map((role) => ({ + value: role, + label: role, + }))} + /> + {isUpdating &&

Updating role...

} +
+ {error && ( +
+

{error}

+
+ )} +
+ ) +}