Reconcile the docs with the shipped implementation. The exploration
flagged three docs as out of sync; one was a scratch file.
- docs/test.md deleted. File content was literally the four bytes
'aaaa' — a debugging leftover that had no business in the repo.
- docs/admin.md rewritten end-to-end. The old file still described
the removed hardcoded super/admin credentials ('SuperAdmin will
log in with hardcoded username: super and password:admin'). The
new version covers roles, the dual-auth model, the
requireAdmin/requireAdminPost/requireSuperAdminPost guard flow,
env-based super-admin provisioning (with the bcrypt-hash generation
command), code generation + the atomic-claim guard, the
Code.createdById onDelete:Restrict decision, and the rate-limit
surface — i.e. all of the Phase 1–6 decisions in one place. It
is now the authoritative source of truth for the admin flow.
- docs/description.md (the 324-line original architecture plan) gets
a banner at the top listing every known delta with the actual
implementation: storage (S3 not UploadThing/Vercel Blob),
upload MIME allow-list, dual auth (Clerk + custom HMAC), production
deploy (Coolify/Traefik instead of Vercel), subdomain routing
(Host header, not X-Subdomain), schema notes for the deliberate
String? bornDate/passedDate, rate limiting + CSRF enforcement, and
in-house QR rendering. The body of the document is preserved as
historical design context.
- docs/adminImplem.md (the 259-line original implementation plan)
gets a similar banner — most notable is that the cookie path is
actually '/' not '/admin', and the rolling helpers in
admin-session.ts supersede the per-route inline guards the doc
describes. Body preserved as history.
No source changes — docs only.
Note: docs/db.md is left untouched per the user's instruction to defer
secret rotation to a later stage. It still contains an in-repo DB
password; that and the other .env secrets will be addressed when
rotation happens.
9.4 KiB
SuperAdmin & Admin + Code Access System — Implementation Plan
⚠ Historical implementation plan — see
docs/admin.mdfor current stateThis was the original plan doc. Several specifics differ from the actual implementation (
src/lib/admin-session.ts,src/app/api/admin/*,src/app/admin/**):
- Cookie path: planned as
/admin; shipped as/(so admin API routes under/api/admin/*receive the cookie).- Super-admin credentials: planned as hardcoded
super/admin; shipped from env (SUPER_ADMIN_USERNAME+SUPER_ADMIN_PASSWORD_HASH, bcrypt) — seedocs/admin.md.- Super-admin row in
AdminUser: planned as optional seed; required by theCode.createdByIdFK and provisioned byprisma/seed.ts.- Rate limiting, CSRF/Origin checks, password complexity, atomic code claim (updateMany guard): all added in Phases 1–6 and not in the original plan.
- All admin API routes now funnel through
requireAdmin/requireAdminPost/requireSuperAdminPostinsrc/lib/admin-session.ts; the rolling-helpers inadmin-session.tssupersede the inlinegetAdminSession()checks described per-route in this top-of-the-doc walkthrough.
Overview
Implement SuperAdmin/admin role management and code-gated memorial creation as outlined in admin.md.
- SuperAdmin: hardcoded username
super, passwordadmin - 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)
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 usingADMIN_SESSION_SECRETenv varverifyAdminSession(token: string): { username: string; role: Role } | null— verify and decodegetAdminSession(): { username: string; role: Role } | null— read fromrequest.cookiesorcookies()- 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"andpassword === "admin"→ set session with roleSUPER_ADMIN - Else query
AdminUserwhereusername === username, compare withbcrypt.compare - Return
{ success: true }and setadmin_sessioncookie (httpOnly, secure, sameSite=lax, path=/admin) - On failure:
401
src/app/api/admin/logout/route.ts (new)
- POST: clear
admin_sessioncookie
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
AdminUserrecord
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
- No/invalid cookie → redirect to
- If path starts with
/api/admin, check admin session cookie- No/invalid cookie → return
401
- No/invalid cookie → return
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/loginif 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
AdminUserwith 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 withcreatedByIdfrom 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
Codewherecode === codeandusedByUserId === 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
- Calls
- 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
403with"Потребен е валиден код за креирање спомен страница"
Phase 5 — Environment & Build
New Env Variable
ADMIN_SESSION_SECRET=<random-64-char-string>
Add to .env.example and docker-compose files.
Build & Verify
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/loginwithsuper/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 |