feat(api): deactivate/reactivate users (ban + revoke sessions) with guards
This commit is contained in:
@@ -193,3 +193,103 @@ describe('POST /api/admin/users/:id/role', () => {
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivate / reactivate', () => {
|
||||
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('blocks sign-in after deactivate and restores it after reactivate', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'deact-admin@example.com', 'admin');
|
||||
// create a worker we control the password of
|
||||
await app.request('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
email: 'deact-target@example.com',
|
||||
name: 'Deact',
|
||||
password: 'sterk-wachtwoord-123',
|
||||
role: 'worker',
|
||||
}),
|
||||
});
|
||||
const id = await userIdByEmail('deact-target@example.com');
|
||||
expect(await signIn(app, 'deact-target@example.com', 'sterk-wachtwoord-123')).toBe(200);
|
||||
|
||||
const off = await app.request(`/api/admin/users/${id}/deactivate`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(off.status).toBe(200);
|
||||
expect((await off.json()).status).toBe('inactive');
|
||||
expect(await signIn(app, 'deact-target@example.com', 'sterk-wachtwoord-123')).not.toBe(200);
|
||||
|
||||
const on = await app.request(`/api/admin/users/${id}/reactivate`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect((await on.json()).status).toBe('active');
|
||||
expect(await signIn(app, 'deact-target@example.com', 'sterk-wachtwoord-123')).toBe(200);
|
||||
});
|
||||
|
||||
it('refuses self-deactivation', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'deact-self@example.com', 'admin');
|
||||
await authToken(app, 'deact-self-other@example.com', 'admin'); // a second admin
|
||||
const id = await userIdByEmail('deact-self@example.com');
|
||||
const res = await app.request(`/api/admin/users/${id}/deactivate`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect((await res.json()).error).toContain('deactiveren');
|
||||
});
|
||||
|
||||
it('refuses deactivating the last active admin', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'deact-last-admin@example.com', 'admin');
|
||||
// promote a peer, deactivate caller is self-blocked; deactivate the peer is allowed (caller remains).
|
||||
await authToken(app, 'deact-peer@example.com', 'admin');
|
||||
const peerId = await userIdByEmail('deact-peer@example.com');
|
||||
const res = await app.request(`/api/admin/users/${peerId}/deactivate`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(res.status).toBe(200); // caller still admin → allowed
|
||||
// Now the peer is inactive; the caller is the only active admin. Deactivating self is blocked:
|
||||
const callerId = await userIdByEmail('deact-last-admin@example.com');
|
||||
const selfRes = await app.request(`/api/admin/users/${callerId}/deactivate`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(selfRes.status).toBe(400); // self-guard (which also preserves the last admin)
|
||||
});
|
||||
|
||||
it('404s on an unknown id and 403s for a worker', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'deact-404-admin@example.com', 'admin');
|
||||
expect(
|
||||
(
|
||||
await app.request('/api/admin/users/nope/deactivate', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
})
|
||||
).status,
|
||||
).toBe(404);
|
||||
const workerTok = await authToken(app, 'deact-worker@example.com');
|
||||
const id = await userIdByEmail('deact-worker@example.com');
|
||||
expect(
|
||||
(
|
||||
await app.request(`/api/admin/users/${id}/deactivate`, {
|
||||
method: 'POST',
|
||||
headers: bearer(workerTok),
|
||||
})
|
||||
).status,
|
||||
).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user