feat(api): role-aware session helper + admin-only activity writes

This commit is contained in:
Bas van Rossem
2026-06-17 17:43:37 +02:00
parent c73fa0f898
commit f2cc0973c7
6 changed files with 78 additions and 48 deletions

View File

@@ -1,8 +1,18 @@
import type { Context } from 'hono';
import { auth } from '../auth';
export async function getSessionUser(c: Context): Promise<{ id: string } | null> {
export interface SessionUser {
id: string;
role: string;
}
export async function getSessionUser(c: Context): Promise<SessionUser | null> {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) return null;
return { id: session.user.id };
const role = (session.user as { role?: string | null }).role ?? 'worker';
return { id: session.user.id, role };
}
export function isAdmin(u: SessionUser | null): boolean {
return u?.role === 'admin';
}

View File

@@ -0,0 +1,27 @@
import type { WorkSession } from '@solelog/shared';
import type { workSessions } from '../db/schema';
type WorkSessionRow = typeof workSessions.$inferSelect;
export function toWorkSession(
row: WorkSessionRow,
opts: { activityName?: string | null; userName?: string | null; userEmail?: string | null } = {}
): WorkSession {
return {
id: row.id,
user_id: row.userId,
activity_id: row.activityId,
activity_name: opts.activityName ?? undefined,
user_name: opts.userName ?? undefined,
user_email: opts.userEmail ?? undefined,
insole_type: (row.insoleType ?? null) as WorkSession['insole_type'],
pair_count: row.pairCount,
start_time: new Date(row.startTime).toISOString(),
end_time: row.endTime ? new Date(row.endTime).toISOString() : null,
duration_seconds: row.durationSeconds ?? null,
status: row.status as WorkSession['status'],
source: row.source as WorkSession['source'],
notes: row.notes ?? null,
created_at: new Date(row.createdAt).toISOString(),
};
}

View File

@@ -4,7 +4,7 @@ import { CreateActivityInput, UpdateActivityInput } from '@solelog/shared';
import type { Activity } from '@solelog/shared';
import { db } from '../db/client';
import { activities, workSessions } from '../db/schema';
import { getSessionUser } from '../lib/require-user';
import { getSessionUser, isAdmin } from '../lib/require-user';
export const activitiesRoutes = new Hono();
@@ -34,6 +34,7 @@ activitiesRoutes.get('/api/activities', async (c) => {
activitiesRoutes.post('/api/activities', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403);
const parsed = CreateActivityInput.safeParse(await c.req.json().catch(() => null));
if (!parsed.success) return c.json({ error: 'Invalid input' }, 400);
@@ -48,6 +49,7 @@ activitiesRoutes.post('/api/activities', async (c) => {
activitiesRoutes.put('/api/activities/:id', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403);
const id = Number.parseInt(c.req.param('id'), 10);
if (Number.isNaN(id)) return c.json({ error: 'Activity not found' }, 404);
@@ -67,6 +69,7 @@ activitiesRoutes.put('/api/activities/:id', async (c) => {
activitiesRoutes.delete('/api/activities/:id', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403);
const id = Number.parseInt(c.req.param('id'), 10);
if (Number.isNaN(id)) return c.json({ error: 'Activity not found' }, 404);

View File

@@ -1,34 +1,14 @@
import { Hono } from 'hono';
import { and, asc, desc, eq } from 'drizzle-orm';
import { StartSessionInput } from '@solelog/shared';
import type { WorkSession } from '@solelog/shared';
import { db } from '../db/client';
import { activities, workSessions } from '../db/schema';
import { getSessionUser } from '../lib/require-user';
import { toWorkSession } from '../lib/work-session';
import { quote, formatDuration } from '../lib/csv';
export const sessionsRoutes = new Hono();
type WorkSessionRow = typeof workSessions.$inferSelect;
function toWorkSession(row: WorkSessionRow, activityName?: string | null): WorkSession {
return {
id: row.id,
user_id: row.userId,
activity_id: row.activityId,
activity_name: activityName ?? undefined,
insole_type: (row.insoleType ?? null) as WorkSession['insole_type'],
pair_count: row.pairCount,
start_time: new Date(row.startTime).toISOString(),
end_time: row.endTime ? new Date(row.endTime).toISOString() : null,
duration_seconds: row.durationSeconds ?? null,
status: row.status as WorkSession['status'],
source: row.source as WorkSession['source'],
notes: row.notes ?? null,
created_at: new Date(row.createdAt).toISOString(),
};
}
sessionsRoutes.get('/api/export', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
@@ -95,7 +75,7 @@ sessionsRoutes.get('/api/sessions/active', async (c) => {
.where(and(eq(workSessions.userId, sessionUser.id), eq(workSessions.status, 'active')))
.orderBy(desc(workSessions.startTime));
return c.json(rows.map((r) => toWorkSession(r.session, r.activityName)));
return c.json(rows.map((r) => toWorkSession(r.session, { activityName: r.activityName })));
});
sessionsRoutes.get('/api/sessions', async (c) => {
@@ -109,7 +89,7 @@ sessionsRoutes.get('/api/sessions', async (c) => {
.where(eq(workSessions.userId, sessionUser.id))
.orderBy(desc(workSessions.startTime));
return c.json(rows.map((r) => toWorkSession(r.session, r.activityName)));
return c.json(rows.map((r) => toWorkSession(r.session, { activityName: r.activityName })));
});
sessionsRoutes.post('/api/sessions/start', async (c) => {
@@ -139,7 +119,7 @@ sessionsRoutes.post('/api/sessions/start', async (c) => {
source: 'app',
})
.returning();
return c.json(toWorkSession(row, activity.name));
return c.json(toWorkSession(row, { activityName: activity.name }));
});
sessionsRoutes.post('/api/sessions/:id/stop', async (c) => {
@@ -157,7 +137,9 @@ sessionsRoutes.post('/api/sessions/:id/stop', async (c) => {
if (row.status !== 'active') return c.json({ error: 'Session already closed' }, 409);
const endTime = new Date();
const durationSeconds = Math.round((endTime.getTime() - new Date(row.startTime).getTime()) / 1000);
const durationSeconds = Math.round(
(endTime.getTime() - new Date(row.startTime).getTime()) / 1000
);
const [updated] = await db
.update(workSessions)