2.3 KiB
2.3 KiB
Admin flow
Roles
- SUPER_ADMIN: provisioned from environment. Has full power — creates and deletes regular admins, generates codes, resets passwords.
- ADMIN: created by a SUPER_ADMIN via the admin panel. Can only generate and delete their own codes.
Authentication
The admin layer uses a custom HMAC-signed cookie, separate from Clerk (which is for end-users only):
POST /api/admin/login— verifies credentials, sets anadmin_sessioncookie signed withADMIN_SESSION_SECRET.POST /api/admin/logout— clears the cookie.src/lib/admin-session.ts— sign/verify helpers, plusrequireAdmin/requireAdminPost/requireSuperAdminPostguards that handle auth + same-origin (CSRF) checks for admin API routes.- All admin POST routes run through
requireAdminPost(orrequireSuperAdminPost), which enforcesOrigin/Referer/HostmatchingNEXT_PUBLIC_APP_URL.
Super-admin provisioning
Username and bcrypt-hashed password are read from env:
SUPER_ADMIN_USERNAME(defaultsuper)SUPER_ADMIN_PASSWORD_HASH— bcrypt hash, NOT plaintext. Generate with:node -e "import('bcryptjs').then(b => b.default.hash('YOUR_PASSWORD', 12).then(console.log))"
Because Code.createdById is a non-nullable FK to AdminUser, the
env super-admin also needs a row in AdminUser. Provisioning is
handled by prisma/seed.cjs, which upserts the SUPER_ADMIN row from
the same env values. Run npm run db:seed after starting the DB; the
container's scripts/start.sh also runs it automatically on every
boot (after prisma migrate deploy).
Codes
- A
Codeis a 12-char uppercase hex string generated byPOST /api/admin/codes. - Users validate a code via
POST /api/validate-code. The claim is atomic (usesupdateManywithusedByUserId: nullguard) so a code can't be claimed by two concurrent requests. Code.createdByIdis a non-nullable FK toAdminUserwithonDelete: Restrict(Prisma default). You cannot delete an admin who has issued codes — they audit-protect the code trail.
Rate limiting + brute-force defence
/api/admin/login: 5 attempts/min per IP (adminLoginLimiter)./api/validate-code: 20 attempts/min per IP (validateCodeLimiter).- Other admin mutating routes are also CSRF-guarded.
- All limiters live in
src/lib/rate-limit.ts.