From 4b80898b0e803b1d4810e18bfc757f21b84e9b2c Mon Sep 17 00:00:00 2001 From: Bas van Rossem Date: Wed, 24 Jun 2026 22:57:35 +0200 Subject: [PATCH] feat(api): admin reset-password endpoint --- apps/api/src/routes/admin-users.ts | 27 ++++++++++++- apps/api/test/admin-users.test.ts | 62 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/admin-users.ts b/apps/api/src/routes/admin-users.ts index fe440e9..e288203 100644 --- a/apps/api/src/routes/admin-users.ts +++ b/apps/api/src/routes/admin-users.ts @@ -1,6 +1,12 @@ import { Hono } from 'hono'; import { asc, eq } from 'drizzle-orm'; -import { CreateUserInput, SetRoleInput, type AdminUser, type Role } from '@solelog/shared'; +import { + CreateUserInput, + SetPasswordInput, + SetRoleInput, + type AdminUser, + type Role, +} from '@solelog/shared'; import { db } from '../db/client'; import { session, user } from '../db/schema'; import { auth } from '../auth'; @@ -120,3 +126,22 @@ adminUsersRoutes.post('/api/admin/users/:id/reactivate', async (c) => { const [updated] = await db.select().from(user).where(eq(user.id, id)); return c.json(toListItem(updated)); }); + +adminUsersRoutes.post('/api/admin/users/:id/password', async (c) => { + const id = c.req.param('id'); + const parsed = SetPasswordInput.safeParse(await c.req.json().catch(() => null)); + if (!parsed.success) return c.json({ error: 'Invalid input' }, 400); + + const [target] = await db.select().from(user).where(eq(user.id, id)); + if (!target) return c.json({ error: 'Gebruiker niet gevonden' }, 404); + + try { + await auth.api.setUserPassword({ + body: { userId: id, newPassword: parsed.data.password }, + headers: c.req.raw.headers, + }); + } catch { + return c.json({ error: 'Wachtwoord wijzigen mislukt.' }, 400); + } + return c.json({ success: true }); +}); diff --git a/apps/api/test/admin-users.test.ts b/apps/api/test/admin-users.test.ts index 9a81752..8c50f75 100644 --- a/apps/api/test/admin-users.test.ts +++ b/apps/api/test/admin-users.test.ts @@ -293,3 +293,65 @@ describe('deactivate / reactivate', () => { ).toBe(403); }); }); + +describe('POST /api/admin/users/:id/password', () => { + async function signIn(app: Hono, email: string, password: string): Promise { + const res = await app.request('/api/auth/sign-in/email', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + return res.status; + } + + it('sets a new password: new works, old fails', async () => { + const app = createApp(); + const adminTok = await authToken(app, 'pw-admin@example.com', 'admin'); + await app.request('/api/admin/users', { + method: 'POST', + headers: bearer(adminTok), + body: JSON.stringify({ + email: 'pw-target@example.com', + name: 'Pw', + password: 'old-wachtwoord-123', + role: 'worker', + }), + }); + const id = await userIdByEmail('pw-target@example.com'); + + const res = await app.request(`/api/admin/users/${id}/password`, { + method: 'POST', + headers: bearer(adminTok), + body: JSON.stringify({ password: 'new-wachtwoord-456' }), + }); + expect(res.status).toBe(200); + expect(await signIn(app, 'pw-target@example.com', 'new-wachtwoord-456')).toBe(200); + expect(await signIn(app, 'pw-target@example.com', 'old-wachtwoord-123')).not.toBe(200); + }); + + it('rejects a too-short password with 400 and 403s for a worker', async () => { + const app = createApp(); + const adminTok = await authToken(app, 'pw-short-admin@example.com', 'admin'); + await authToken(app, 'pw-worker@example.com'); + const id = await userIdByEmail('pw-worker@example.com'); + expect( + ( + await app.request(`/api/admin/users/${id}/password`, { + method: 'POST', + headers: bearer(adminTok), + body: JSON.stringify({ password: 'short' }), + }) + ).status, + ).toBe(400); + const workerTok = await authToken(app, 'pw-worker2@example.com'); + expect( + ( + await app.request(`/api/admin/users/${id}/password`, { + method: 'POST', + headers: bearer(workerTok), + body: JSON.stringify({ password: 'long-enough-123' }), + }) + ).status, + ).toBe(403); + }); +});