feat(api): user-scoped activities CRUD with shared auth helper

This commit is contained in:
Bas van Rossem
2026-06-17 15:34:42 +02:00
parent 57809985fd
commit 5e61b7720d
4 changed files with 286 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
import { Hono } from 'hono';
import { eq, asc } from 'drizzle-orm';
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';
export const activitiesRoutes = new Hono();
type ActivityRow = typeof activities.$inferSelect;
function toActivity(row: ActivityRow): Activity {
return {
id: row.id,
name: row.name,
insole_types: row.insoleTypes as Activity['insole_types'],
created_at: new Date(row.createdAt).toISOString(),
};
}
activitiesRoutes.get('/api/activities', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
const rows = await db.select().from(activities).orderBy(asc(activities.name));
const insoleType = c.req.query('insole_type');
const filtered = insoleType
? rows.filter((r) => (r.insoleTypes as string[]).includes(insoleType))
: rows;
return c.json(filtered.map(toActivity));
});
activitiesRoutes.post('/api/activities', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
const parsed = CreateActivityInput.safeParse(await c.req.json().catch(() => null));
if (!parsed.success) return c.json({ error: 'Invalid input' }, 400);
const [row] = await db
.insert(activities)
.values({ name: parsed.data.name, insoleTypes: parsed.data.insole_types })
.returning();
return c.json(toActivity(row));
});
activitiesRoutes.put('/api/activities/:id', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
const id = Number.parseInt(c.req.param('id'), 10);
if (Number.isNaN(id)) return c.json({ error: 'Activity not found' }, 404);
const parsed = UpdateActivityInput.safeParse(await c.req.json().catch(() => null));
if (!parsed.success) return c.json({ error: 'Invalid input' }, 400);
const [row] = await db
.update(activities)
.set({ name: parsed.data.name, insoleTypes: parsed.data.insole_types })
.where(eq(activities.id, id))
.returning();
if (!row) return c.json({ error: 'Activity not found' }, 404);
return c.json(toActivity(row));
});
activitiesRoutes.delete('/api/activities/:id', async (c) => {
const sessionUser = await getSessionUser(c);
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
const id = Number.parseInt(c.req.param('id'), 10);
if (Number.isNaN(id)) return c.json({ error: 'Activity not found' }, 404);
// FK has no cascade declared: delete the activity's sessions first, then the activity.
await db.delete(workSessions).where(eq(workSessions.activityId, id));
await db.delete(activities).where(eq(activities.id, id));
return c.json({ success: true });
});