feat(api): admin-users router with enriched roster + user creation

This commit is contained in:
Bas van Rossem
2026-06-24 22:46:59 +02:00
parent a32406de69
commit 2b4b633961
4 changed files with 174 additions and 9 deletions

View File

@@ -5,6 +5,7 @@ import { me } from './routes/me';
import { activitiesRoutes } from './routes/activities';
import { sessionsRoutes } from './routes/sessions';
import { adminRoutes } from './routes/admin';
import { adminUsersRoutes } from './routes/admin-users';
import { auth } from './auth';
import { env } from './env';
@@ -26,5 +27,6 @@ export function createApp(): Hono {
app.route('/', activitiesRoutes);
app.route('/', sessionsRoutes);
app.route('/', adminRoutes);
app.route('/', adminUsersRoutes);
return app;
}

View File

@@ -0,0 +1,56 @@
import { Hono } from 'hono';
import { asc, eq } from 'drizzle-orm';
import { CreateUserInput, type AdminUser, type Role } from '@solelog/shared';
import { db } from '../db/client';
import { session, user } from '../db/schema';
import { auth } from '../auth';
import { adminGuard } from '../lib/require-user';
export const adminUsersRoutes = new Hono();
adminUsersRoutes.use('/api/admin/users', adminGuard);
adminUsersRoutes.use('/api/admin/users/*', adminGuard);
type UserRow = typeof user.$inferSelect;
// Map a user row to the public list shape.
export function toListItem(row: UserRow): AdminUser {
return {
id: row.id,
email: row.email,
name: row.name,
role: (row.role ?? 'worker') as Role,
status: row.banned ? 'inactive' : 'active',
created_at: new Date(row.createdAt).toISOString(),
};
}
// Enriched roster (replaces the old id/name/email version from admin.ts).
adminUsersRoutes.get('/api/admin/users', async (c) => {
const rows = await db.select().from(user).orderBy(asc(user.name));
return c.json(rows.map(toListItem));
});
// Create a user. role/password validated by zod; password hashing via better-auth.
adminUsersRoutes.post('/api/admin/users', async (c) => {
const parsed = CreateUserInput.safeParse(await c.req.json().catch(() => null));
if (!parsed.success) return c.json({ error: 'Invalid input' }, 400);
const d = parsed.data;
// better-auth types role as its built-in set; our 'worker' role is valid at runtime (same cast as seed.ts).
const createUser = auth.api.createUser as (args: {
body: { email: string; password: string; name: string; role: 'worker' | 'admin' };
}) => Promise<unknown>;
try {
await createUser({
body: { email: d.email, password: d.password, name: d.name, role: d.role },
});
} catch {
// Inputs are pre-validated, so the realistic failure is a duplicate email.
return c.json({ error: 'E-mailadres bestaat al.' }, 409);
}
const [row] = await db.select().from(user).where(eq(user.email, d.email));
return c.json(toListItem(row));
});

View File

@@ -105,15 +105,6 @@ adminRoutes.get('/api/admin/sessions/active', async (c) => {
);
});
// Roster for the create-form worker picker. Direct DB read — no better-auth client dependency.
adminRoutes.get('/api/admin/users', async (c) => {
const rows = await db
.select({ id: user.id, name: user.name, email: user.email })
.from(user)
.orderBy(asc(user.name));
return c.json(rows);
});
adminRoutes.get('/api/admin/report', async (c) => {
const q = parseReportQuery(c);
if (!q) return c.json({ error: 'Invalid query' }, 400);