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 && (
+
+ )}
+
+ )
+}