feat(auth): admin user management service, routes, and integration tests
This commit is contained in:
96
packages/backend/src/tests/admin-users.integration.test.ts
Normal file
96
packages/backend/src/tests/admin-users.integration.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import { createApp } from '../app.js';
|
||||
import { makeTestDb, createUserDirect } from './dbHelper.js';
|
||||
import { hashPassword } from '../services/auth/passwords.js';
|
||||
import { setMailerForTests, type Mailer } from '../services/auth/email.js';
|
||||
|
||||
class CaptureMailer implements Mailer {
|
||||
sent: { to: string; subject: string; text: string }[] = [];
|
||||
async send(to: string, m: { subject: string; html: string; text: string }) {
|
||||
this.sent.push({ to, subject: m.subject, text: m.text });
|
||||
}
|
||||
}
|
||||
|
||||
async function loginAs(app: ReturnType<typeof createApp>, email: string, password = 'secretpass') {
|
||||
const r = await request(app).post('/api/auth/login').send({ email, password });
|
||||
if (r.status !== 200) throw new Error(`login failed: ${r.status} ${JSON.stringify(r.body)}`);
|
||||
const cookies = (r.headers['set-cookie'] as unknown as string[]);
|
||||
const csrf = cookies.find((c) => c.startsWith('flashcard_csrf='))!.split(';')[0]!.split('=')[1]!;
|
||||
return { cookies, csrf };
|
||||
}
|
||||
|
||||
async function makeAdmin(env: ReturnType<typeof makeTestDb>, email: string) {
|
||||
return createUserDirect(env.db, {
|
||||
email, role: 'sysadmin', isActive: true,
|
||||
passwordHash: await hashPassword('secretpass'),
|
||||
emailVerifiedAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
|
||||
let env: ReturnType<typeof makeTestDb>;
|
||||
let mailer: CaptureMailer;
|
||||
let app: ReturnType<typeof createApp>;
|
||||
|
||||
beforeEach(async () => {
|
||||
env = makeTestDb();
|
||||
mailer = new CaptureMailer();
|
||||
setMailerForTests(mailer);
|
||||
app = createApp(env.db);
|
||||
});
|
||||
|
||||
describe('admin users', () => {
|
||||
it('invites a user, target accepts and can log in', async () => {
|
||||
await makeAdmin(env, 'admin@example.com');
|
||||
const { cookies, csrf } = await loginAs(app, 'admin@example.com');
|
||||
|
||||
const inv = await request(app).post('/api/admin/users/invite')
|
||||
.set('Cookie', cookies).set('x-csrf-token', csrf)
|
||||
.send({ email: 'newbie@example.com', role: 'user' });
|
||||
expect(inv.status).toBe(201);
|
||||
expect(mailer.sent).toHaveLength(1);
|
||||
const link = mailer.sent[0]!.text;
|
||||
const token = decodeURIComponent(link.match(/token=([^\s&"]+)/)![1]!);
|
||||
|
||||
const accept = await request(app).post('/api/auth/accept-invite')
|
||||
.send({ token, displayName: 'Newbie', password: 'anotherpass' });
|
||||
expect(accept.status).toBe(200);
|
||||
expect(accept.body.email).toBe('newbie@example.com');
|
||||
});
|
||||
|
||||
it('lists users with filtering', async () => {
|
||||
await makeAdmin(env, 'admin@example.com');
|
||||
await createUserDirect(env.db, { email: 'a@example.com', role: 'user' });
|
||||
await createUserDirect(env.db, { email: 'b@example.com', role: 'user' });
|
||||
const { cookies } = await loginAs(app, 'admin@example.com');
|
||||
const list = await request(app).get('/api/admin/users?q=a').set('Cookie', cookies);
|
||||
expect(list.status).toBe(200);
|
||||
expect(list.body.rows.find((u: { email: string }) => u.email === 'a@example.com')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('blocks demoting the last sysadmin', async () => {
|
||||
const admin = await makeAdmin(env, 'admin@example.com');
|
||||
const { cookies, csrf } = await loginAs(app, 'admin@example.com');
|
||||
const r = await request(app).patch(`/api/admin/users/${admin.id}`)
|
||||
.set('Cookie', cookies).set('x-csrf-token', csrf)
|
||||
.send({ role: 'user' });
|
||||
expect(r.status).toBe(409);
|
||||
expect(r.body.error.code).toBe('LAST_SYSADMIN');
|
||||
});
|
||||
|
||||
it('rejects non-admin access to admin routes', async () => {
|
||||
await createUserDirect(env.db, {
|
||||
email: 'plain@example.com', role: 'user', isActive: true,
|
||||
passwordHash: await hashPassword('secretpass'),
|
||||
emailVerifiedAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
const { cookies } = await loginAs(app, 'plain@example.com');
|
||||
const r = await request(app).get('/api/admin/users').set('Cookie', cookies);
|
||||
expect(r.status).toBe(403);
|
||||
});
|
||||
|
||||
it('blocks unauthenticated access to admin routes', async () => {
|
||||
const r = await request(app).get('/api/admin/users');
|
||||
expect(r.status).toBe(401);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user