--- plan: 36-01 phase: 36 title: "isAdmin schema, requireAdmin middleware, /api/auth/me surface, grant script" status: complete completed: 2026-04-19 --- ## What Was Built Server-side admin foundation for Phase 36: 1. **isAdmin column** added to the `users` pgTable in `src/db/schema.ts` — `boolean("is_admin").notNull().default(false)`. 2. **Drizzle migration** generated (`drizzle-pg/0009_spotty_lord_tyger.sql`) with `ALTER TABLE "users" ADD COLUMN "is_admin" boolean DEFAULT false NOT NULL`. DB push could not be applied (DB not reachable with default credentials — requires `DATABASE_URL` env var pointing to the running Postgres instance). 3. **requireAdmin middleware** added to `src/server/middleware/auth.ts` — reads `userId` from context (set by `requireAuth`), queries `users.isAdmin`, returns 401 if userId missing, 403 if `!user.isAdmin`, calls `next()` for admins. 4. **isAdmin in /api/auth/me** — `src/server/routes/auth.ts` now includes `isAdmin: fullUser?.isAdmin ?? false` in the returned user object. 5. **`/api/admin/` placeholder route** — `src/server/routes/admin.ts` applies `requireAuth` + `requireAdmin` middleware on `/*` and returns `{ ok: true }` on `GET /`. 6. **Route registration** — `src/server/index.ts` imports and registers `app.route("/api/admin", adminRoutes)`. 7. **grant-admin script** — `scripts/grant-admin.ts` grants or revokes `isAdmin` by `logto_sub`. Accepts `--revoke` flag. Exits 1 on missing sub or user not found. ## Key Files - `src/db/schema.ts` — isAdmin column added to users table - `drizzle-pg/0009_spotty_lord_tyger.sql` — migration file - `src/server/middleware/auth.ts` — requireAdmin exported - `src/server/routes/auth.ts` — isAdmin in /me response - `src/server/routes/admin.ts` — new placeholder admin route - `src/server/index.ts` — adminRoutes registered - `scripts/grant-admin.ts` — admin grant/revoke script ## Deviations - **DB push could not be applied** — the default PostgreSQL credentials (`gearbox:gearbox@localhost:5432/gearbox`) don't match the running instance. The migration file is generated and correct. Apply manually with the correct `DATABASE_URL`: ``` DATABASE_URL= bun run db:push ``` This is a deployment/environment concern, not a code defect. ## Self-Check: PASSED - [x] isAdmin column in schema.ts - [x] Migration file generated with correct SQL - [x] requireAdmin middleware exported from auth.ts - [x] isAdmin in /api/auth/me response - [x] /api/admin route protected by requireAuth + requireAdmin - [x] grant-admin.ts script created - [x] bun run build exits 0