feat(api): user-scoped activities CRUD with shared auth helper
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { health } from './routes/health';
|
||||
import { me } from './routes/me';
|
||||
import { activitiesRoutes } from './routes/activities';
|
||||
import { auth } from './auth';
|
||||
|
||||
export function createApp(): Hono {
|
||||
@@ -8,5 +9,6 @@ export function createApp(): Hono {
|
||||
app.route('/', health);
|
||||
app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw));
|
||||
app.route('/', me);
|
||||
app.route('/', activitiesRoutes);
|
||||
return app;
|
||||
}
|
||||
|
||||
8
apps/api/src/lib/require-user.ts
Normal file
8
apps/api/src/lib/require-user.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Context } from 'hono';
|
||||
import { auth } from '../auth';
|
||||
|
||||
export async function getSessionUser(c: Context): Promise<{ id: string } | null> {
|
||||
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
||||
if (!session) return null;
|
||||
return { id: session.user.id };
|
||||
}
|
||||
78
apps/api/src/routes/activities.ts
Normal file
78
apps/api/src/routes/activities.ts
Normal 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 });
|
||||
});
|
||||
Reference in New Issue
Block a user