refactor(api): extract adminGuard + add user-management contracts

This commit is contained in:
Bas van Rossem
2026-06-24 22:42:35 +02:00
parent 4463a2e41a
commit a32406de69
3 changed files with 29 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
import type { Context } from 'hono'; import type { Context, MiddlewareHandler } from 'hono';
import { auth } from '../auth'; import { auth } from '../auth';
export interface SessionUser { export interface SessionUser {
@@ -16,3 +16,11 @@ export async function getSessionUser(c: Context): Promise<SessionUser | null> {
export function isAdmin(u: SessionUser | null): boolean { export function isAdmin(u: SessionUser | null): boolean {
return u?.role === 'admin'; return u?.role === 'admin';
} }
// Reusable gate for the whole /api/admin/* surface: 401 if unauthenticated, 403 if not an admin.
export const adminGuard: MiddlewareHandler = async (c, next) => {
const u = await getSessionUser(c);
if (!u) return c.json({ error: 'Unauthorized' }, 401);
if (!isAdmin(u)) return c.json({ error: 'Forbidden' }, 403);
await next();
};

View File

@@ -3,7 +3,7 @@ import { and, asc, desc, eq, gte, lte, type SQL } from 'drizzle-orm';
import { AdminUpdateSessionInput, CreateManualSessionInput, InsoleType } from '@solelog/shared'; import { AdminUpdateSessionInput, CreateManualSessionInput, InsoleType } from '@solelog/shared';
import { db } from '../db/client'; import { db } from '../db/client';
import { activities, user, workSessions } from '../db/schema'; import { activities, user, workSessions } from '../db/schema';
import { getSessionUser, isAdmin } from '../lib/require-user'; import { adminGuard } from '../lib/require-user';
import { toWorkSession } from '../lib/work-session'; import { toWorkSession } from '../lib/work-session';
import { buildSessionsCsv } from '../lib/csv'; import { buildSessionsCsv } from '../lib/csv';
@@ -59,12 +59,7 @@ function buildSessionFilters(q: ReportQuery): SQL[] {
export const adminRoutes = new Hono(); export const adminRoutes = new Hono();
// Gate the whole /api/admin/* surface to admins. // Gate the whole /api/admin/* surface to admins.
adminRoutes.use('/api/admin/*', async (c, next) => { adminRoutes.use('/api/admin/*', adminGuard);
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403);
await next();
});
const baseSelect = { const baseSelect = {
session: workSessions, session: workSessions,

View File

@@ -78,15 +78,33 @@ export const StartSessionInput = z.object({
}); });
export type StartSessionInput = z.infer<typeof StartSessionInput>; export type StartSessionInput = z.infer<typeof StartSessionInput>;
export const UserStatus = z.enum(['active', 'inactive']);
export type UserStatus = z.infer<typeof UserStatus>;
export const AdminUser = z.object({ export const AdminUser = z.object({
id: z.string(), id: z.string(),
email: z.string().email(), email: z.string().email(),
name: z.string(), name: z.string(),
role: Role, role: Role,
status: UserStatus,
created_at: z.string(), created_at: z.string(),
}); });
export type AdminUser = z.infer<typeof AdminUser>; export type AdminUser = z.infer<typeof AdminUser>;
export const CreateUserInput = z.object({
email: z.string().email(),
name: z.string().trim().min(1),
password: z.string().min(8),
role: Role,
});
export type CreateUserInput = z.infer<typeof CreateUserInput>;
export const SetRoleInput = z.object({ role: Role });
export type SetRoleInput = z.infer<typeof SetRoleInput>;
export const SetPasswordInput = z.object({ password: z.string().min(8) });
export type SetPasswordInput = z.infer<typeof SetPasswordInput>;
export const CreateManualSessionInput = z.object({ export const CreateManualSessionInput = z.object({
user_id: z.string(), user_id: z.string(),
activity_id: z.number().int(), activity_id: z.number().int(),