admin.ts33 lines · main
| 1 | import { ForbiddenError, UnauthorizedError } from '@briven/shared'; |
| 2 | import { eq } from 'drizzle-orm'; |
| 3 | import type { MiddlewareHandler } from 'hono'; |
| 4 | |
| 5 | import { getDb } from '../db/client.js'; |
| 6 | import { users } from '../db/schema.js'; |
| 7 | import { isSuperadminEmail } from '../lib/superadmin.js'; |
| 8 | import type { User } from './session.js'; |
| 9 | |
| 10 | /** |
| 11 | * Require that the authenticated user has `isAdmin = true` AND — when |
| 12 | * BRIVEN_SUPERADMIN_EMAILS is set — that their email is on the env-pinned |
| 13 | * allowlist (lib/superadmin.ts). Mount on every /v1/admin/* route. |
| 14 | * |
| 15 | * Note: step-up auth (2FA/password re-entry within last 10 min) is |
| 16 | * enforced by `requireFreshAuth` in a separate middleware — this one only |
| 17 | * verifies the admin bit. |
| 18 | */ |
| 19 | export const requireAdmin = (): MiddlewareHandler => async (c, next) => { |
| 20 | const user = c.get('user') as User | null; |
| 21 | if (!user) throw new UnauthorizedError(); |
| 22 | |
| 23 | const db = getDb(); |
| 24 | const [row] = await db |
| 25 | .select({ email: users.email, isAdmin: users.isAdmin, suspendedAt: users.suspendedAt }) |
| 26 | .from(users) |
| 27 | .where(eq(users.id, user.id)) |
| 28 | .limit(1); |
| 29 | if (!row?.isAdmin || !isSuperadminEmail(row.email)) throw new ForbiddenError('admin only'); |
| 30 | if (row.suspendedAt) throw new ForbiddenError('account suspended'); |
| 31 | |
| 32 | await next(); |
| 33 | }; |