feat(api): admin reset-password endpoint
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { asc, eq } from 'drizzle-orm';
|
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 { db } from '../db/client';
|
||||||
import { session, user } from '../db/schema';
|
import { session, user } from '../db/schema';
|
||||||
import { auth } from '../auth';
|
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));
|
const [updated] = await db.select().from(user).where(eq(user.id, id));
|
||||||
return c.json(toListItem(updated));
|
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 });
|
||||||
|
});
|
||||||
|
|||||||
@@ -293,3 +293,65 @@ describe('deactivate / reactivate', () => {
|
|||||||
).toBe(403);
|
).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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user