Some checks are pending
CI / build (push) Waiting to run
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.
283 lines
9.4 KiB
Markdown
283 lines
9.4 KiB
Markdown
# SuperAdmin & Admin + Code Access System — Implementation Plan
|
||
|
||
> ## ⚠ Historical implementation plan — see `docs/admin.md` for current state
|
||
>
|
||
> This 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) — see `docs/admin.md`.
|
||
> - **Super-admin row in `AdminUser`**: planned as optional seed;
|
||
> required by the `Code.createdById` FK and provisioned by
|
||
> `prisma/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` / `requireSuperAdminPost` in
|
||
> `src/lib/admin-session.ts`; the rolling-helpers in
|
||
> `admin-session.ts` supersede the inline `getAdminSession()` 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`, 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=<random-64-char-string>
|
||
```
|
||
|
||
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 |
|