From 868aed4f1065db2aac7ba4720a2aa2afc467ac91 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 19 Apr 2026 20:50:57 +0200 Subject: [PATCH] docs(36): add code review report --- .../36-REVIEW.md | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .planning/phases/36-admin-role-panel-foundation/36-REVIEW.md diff --git a/.planning/phases/36-admin-role-panel-foundation/36-REVIEW.md b/.planning/phases/36-admin-role-panel-foundation/36-REVIEW.md new file mode 100644 index 0000000..b55aefc --- /dev/null +++ b/.planning/phases/36-admin-role-panel-foundation/36-REVIEW.md @@ -0,0 +1,105 @@ +--- +phase: 36 +status: warnings +depth: standard +files_reviewed: 9 +findings: + critical: 0 + warning: 2 + info: 2 + total: 4 +reviewed: 2026-04-19 +--- + +# Code Review — Phase 36: Admin Role & Panel Foundation + +**Files reviewed (9):** +- src/db/schema.ts +- src/server/middleware/auth.ts +- src/server/routes/admin.ts +- src/server/routes/auth.ts +- src/server/index.ts +- src/client/hooks/useAuth.ts +- src/client/routes/admin.tsx +- src/client/routes/admin/index.tsx +- src/client/components/UserMenu.tsx +- scripts/grant-admin.ts + +--- + +## Findings + +### WR-01 — `requireAdmin` can be called without `requireAuth` — no enforcement of ordering [warning] + +**File:** `src/server/middleware/auth.ts` + +**Issue:** `requireAdmin` reads `c.get("userId")` which is only set if `requireAuth` ran first. If a future route registers `requireAdmin` alone (omitting `requireAuth`), `userId` will be undefined and the function returns a 401 — but it doesn't query the DB, so the IS NOT logged as an unauthorized admin attempt. The 401 is correct but the guard works by coincidence rather than explicit enforcement. + +**Risk:** Low in current code (admin.ts correctly chains both). Medium for future route authors who may only apply `requireAdmin`. + +**Recommendation:** Add a JSDoc comment above `requireAdmin` documenting that it must be preceded by `requireAuth`, or verify `userId` is non-null with a named guard: +```typescript +/** + * Requires admin role. MUST be used after requireAuth — depends on userId set in context. + */ +export async function requireAdmin(c: Context, next: Next) { +``` + +--- + +### WR-02 — `admin.tsx` guard has a flash window during auth loading [warning] + +**File:** `src/client/routes/admin.tsx` + +**Issue:** The component returns `null` while `isLoading` is true and while `!auth?.user?.isAdmin`. This means: +1. Admin user: renders nothing → then renders shell (good, no flash of wrong content) +2. Non-admin user: renders nothing → `useEffect` fires redirect → navigates away +3. Unauthenticated user: renders nothing → `useEffect` fires redirect → navigates away + +The issue is case 2/3: between initial render and the `useEffect` execution on the next tick, an unauthenticated or non-admin user briefly renders `null`. This is acceptable UX but means there's a one-tick window where `useEffect` hasn't fired yet. In practice this is invisible, but it should be noted that the guard is async (effect fires after paint). + +**Risk:** UX only — no security impact (server enforces 403 on all `/api/admin/*` endpoints). + +**Recommendation:** Current implementation is acceptable. For completeness, could add a loading spinner while `isLoading` is true, but this is optional. + +--- + +### INFO-01 — `type Env` uses `any` for `db` in admin.ts [info] + +**File:** `src/server/routes/admin.ts` + +**Issue:** `type Env = { Variables: { db?: any; userId?: number } }` uses `any` for `db`. Other routes in the codebase use the same pattern (consistent), but `any` loses type safety for DB operations. + +**Recommendation:** This is a pre-existing pattern across the codebase — acceptable for now. Future refactor could use the typed `LibSQLDatabase` or `DrizzlePostgres` type from the db module. + +--- + +### INFO-02 — `grant-admin.ts` script has no DB connection cleanup [info] + +**File:** `scripts/grant-admin.ts` + +**Issue:** The script imports `db` from `../src/db/index.ts` which creates a `postgres` connection pool. The script doesn't explicitly call `postgres.end()` after the update. With Bun, the process exits cleanly after top-level await, but the connection pool may log a warning or leave a dangling connection in some environments. + +**Risk:** Negligible for a CLI script — process exit terminates all connections. This is standard for Bun one-shot scripts. + +**Recommendation:** No action required. Optionally add `process.exit(0)` at the end to make intent explicit, but not necessary. + +--- + +## Security Assessment + +- **isAdmin defaults to false** — correct, new users cannot be admins by default (NOT NULL DEFAULT false) +- **requireAdmin always queries live DB** — no caching, stale flag not possible server-side +- **No public endpoint exposes admin promotion** — grant-admin.ts requires server access +- **Client-side isAdmin is decorative only** — server enforces 403 on all /api/admin/* routes; client guard is UX only +- **requireAuth + requireAdmin chain** — correctly applied on all /* in admin.ts via `app.use("/*", requireAuth, requireAdmin)` + +No critical security vulnerabilities found. + +--- + +## Summary + +Phase 36 implementation is solid. The two warnings are low-risk: WR-01 is a documentation/future-author concern and WR-02 has no security impact. Both INFO items are non-blocking. The security model is correctly layered (server enforces, client is decorative). + +**Verdict: Ready to proceed to verification.**