# SuperAdmin & Admin + Code Access System — Implementation Plan ## Overview Implement SuperAdmin/admin role management and code-gated memorial creation as outlined in `admin.md`. - SuperAdmin: hardcoded username `super`, password `admin` - SuperAdmin creates admin accounts - Admins generate access codes - Users must enter a valid code during onboarding to create memories --- ## Phase 1 — Database & Dependencies ### New Prisma Models (`prisma/schema.prisma`) ```prisma enum Role { SUPER_ADMIN ADMIN } model AdminUser { id String @id @default(cuid()) username String @unique passwordHash String role Role @default(ADMIN) createdAt DateTime @default(now()) createdCodes Code[] } model Code { id String @id @default(cuid()) code String @unique createdById String createdBy AdminUser @relation(fields: [createdById], references: [id]) usedByUserId String? // User.clerkId usedAt DateTime? createdAt DateTime @default(now()) expiresAt DateTime? @@index([code]) @@index([usedByUserId]) } ``` ### New Dependencies - `bcryptjs` + `@types/bcryptjs` — hash admin passwords ### Seed SuperAdmin Seed script or migration that creates the SuperAdmin `AdminUser` record with hashed password. However, the login itself checks hardcoded `super`/`admin` first (and also queries DB by role for token auth), so the seed is optional — used mainly for listing in the admin panel. ### Migration `npx prisma migrate dev --name add_admin_and_code` --- ## Phase 2 — Admin Auth Admin auth is separate from Clerk. Uses a signed HMAC cookie. ### `src/lib/admin-session.ts` (new) Helpers: - `createAdminSession(username: string, role: Role): string` — sign a cookie value with HMAC-SHA256 using `ADMIN_SESSION_SECRET` env var - `verifyAdminSession(token: string): { username: string; role: Role } | null` — verify and decode - `getAdminSession(): { username: string; role: Role } | null` — read from `request.cookies` or `cookies()` - Cookie name: `admin_session` Payload: `{ username, role, iat }` serialized + HMAC signature. ### `src/app/api/admin/login/route.ts` (new) - POST: accept `{ username, password }` - If `username === "super"` and `password === "admin"` → set session with role `SUPER_ADMIN` - Else query `AdminUser` where `username === username`, compare with `bcrypt.compare` - Return `{ success: true }` and set `admin_session` cookie (httpOnly, secure, sameSite=lax, path=/admin) - On failure: `401` ### `src/app/api/admin/logout/route.ts` (new) - POST: clear `admin_session` cookie ### `src/app/api/admin/change-password/route.ts` (new) - POST: accept `{ currentPassword, newPassword }` - Verify admin session, then verify current password against DB - Hash new password, update `AdminUser` record ### `src/app/admin/login/page.tsx` (new) - Macedonian UI: username/password form - On submit → `POST /api/admin/login` - On success → redirect to `/admin/dashboard` - Show error on failure ### `src/middleware.ts` (modify) - Add `/admin(.*)` and `/api/admin(.*)` to the Clerk exclude list - Before Clerk middleware runs: if path starts with `/admin`, check admin session cookie - No/invalid cookie → redirect to `/admin/login` - Valid → allow - If path starts with `/api/admin`, check admin session cookie - No/invalid cookie → return `401` --- ## Phase 3 — Admin Panel All UI in Macedonian. Layout with sidebar navigation. ### `src/app/admin/layout.tsx` (new) - Checks admin session (server component) - Redirects to `/admin/login` if not authenticated - Provides sidebar with links: Dashboard, Users (SuperAdmin only), Codes - Logout button ### `src/app/admin/dashboard/page.tsx` (new) - Stats cards: - Total AdminUsers count - Total Codes generated - Codes used vs unused - Simple overview ### `src/app/admin/users/page.tsx` (new) - Accessible only to `SUPER_ADMIN` - Table of admin users (username, role, created at) - Button to create new admin (modal/page with username + password fields) - Button to delete admin (with confirmation) - Inline password reset option ### `src/app/admin/users/create/page.tsx` or modal (new) - Form: username, password (with confirmation) - POST to `/api/admin/users/` ### `src/app/admin/codes/page.tsx` (new) - "Generate Code" button → POST `/api/admin/codes` → returns new code string, displays it - Table of all codes: code value, who created it, status (used/unused), used by, used at - Admin can see their own codes. SuperAdmin sees all. - Delete code button ### `src/app/api/admin/users/route.ts` (new) - GET: list all `AdminUser` (SuperAdmin only) - POST: create `AdminUser` with hashed password (SuperAdmin only) ### `src/app/api/admin/users/[id]/route.ts` (new) - DELETE: remove `AdminUser` (SuperAdmin only) - PUT: reset password (SuperAdmin only) ### `src/app/api/admin/codes/route.ts` (new) - GET: list codes (Admins see their own, SuperAdmin sees all) - POST: generate a new code (`crypto.randomBytes(6).toString('hex').toUpperCase()` → 12 chars), store with `createdById` from session ### `src/app/api/admin/codes/[id]/route.ts` (new) - DELETE: remove unused code --- ## Phase 4 — Code Gating ### `src/app/api/validate-code/route.ts` (new) - Requires Clerk auth (`auth()` from `@clerk/nextjs/server`) - POST: `{ code: string }` - Look up `Code` where `code === code` and `usedByUserId === null` - If not found → `{ valid: false, error: "Невалиден или веќе искористен код" }` - If found → update `usedByUserId = userId`, `usedAt = now()` - Return `{ valid: true }` ### `src/app/onboarding/page.tsx` (modify) - Add `"Код"` as step 0 (before "Податоци") - STEPS becomes: `["Код", "Податоци", "Датуми", "Фотографии", "Поддомен", "Шаблон"]` - Step 0: single input for code + "Потврди" button - Calls `POST /api/validate-code` - On success → shows green checkmark, enables "Продолжи" - On failure → shows error, blocks progression - Once validated, user can proceed to step 1 - `canProceed()` for step 0 returns true only if code validated successfully ### `src/app/api/publish/route.ts` (modify) - After Clerk auth check, query: `prisma.code.findFirst({ where: { usedByUserId: userId } })` - If no code found → return `403` with `"Потребен е валиден код за креирање спомен страница"` --- ## Phase 5 — Environment & Build ### New Env Variable ``` ADMIN_SESSION_SECRET= ``` Add to `.env.example` and `docker-compose` files. ### Build & Verify ```bash npm install bcryptjs npm install -D @types/bcryptjs npx prisma migrate dev --name add_admin_and_code npm run build ``` Verify: - [ ] Admin login at `/admin/login` with `super`/`admin` - [ ] Create admin user from admin panel - [ ] Login as created admin - [ ] Generate code from admin panel - [ ] Sign up as new user via Clerk - [ ] Enter code in onboarding step 0 - [ ] Proceed through onboarding and publish - [ ] Verify publish fails without valid code --- ## File Change Summary ### New Files | Path | Purpose | |------|---------| | `src/lib/admin-session.ts` | HMAC cookie helpers | | `src/app/admin/login/page.tsx` | Admin login form | | `src/app/admin/layout.tsx` | Admin layout with sidebar | | `src/app/admin/dashboard/page.tsx` | Dashboard stats | | `src/app/admin/users/page.tsx` | Admin user management | | `src/app/admin/codes/page.tsx` | Code generation & listing | | `src/app/api/admin/login/route.ts` | Admin login API | | `src/app/api/admin/logout/route.ts` | Admin logout API | | `src/app/api/admin/change-password/route.ts` | Self-service password change | | `src/app/api/admin/users/route.ts` | List/create admin users | | `src/app/api/admin/users/[id]/route.ts` | Delete/reset-password admin user | | `src/app/api/admin/codes/route.ts` | List/create codes | | `src/app/api/admin/codes/[id]/route.ts` | Delete code | | `src/app/api/validate-code/route.ts` | Public code validation | ### Modified Files | Path | Change | |------|--------| | `prisma/schema.prisma` | Add `AdminUser` and `Code` models | | `src/middleware.ts` | Exclude `/admin/*` from Clerk, add admin session check | | `src/app/onboarding/page.tsx` | Add code step (step 0) | | `src/app/api/publish/route.ts` | Add code usage check before publish |