admin.ts33 lines · main
1import { ForbiddenError, UnauthorizedError } from '@briven/shared';
2import { eq } from 'drizzle-orm';
3import type { MiddlewareHandler } from 'hono';
4
5import { getDb } from '../db/client.js';
6import { users } from '../db/schema.js';
7import { isSuperadminEmail } from '../lib/superadmin.js';
8import 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 */
19export 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};