feat(api): admin reset-password endpoint

This commit is contained in:
Bas van Rossem
2026-06-24 22:57:35 +02:00
parent 40977f8abd
commit 4b80898b0e
2 changed files with 88 additions and 1 deletions

View File

@@ -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 });
});

View File

@@ -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<number> {
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);
});
});