Ship admin auth, user overview, assessments, chats, assignment filters/export, security hardening, and a separate desktop admin shell with clearer assessment answers. Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
6.3 KiB
TypeScript
167 lines
6.3 KiB
TypeScript
/**
|
|
* Isolated Stage-2 admin API smoke (creates + deletes its own users).
|
|
* Usage: npx tsx scripts/validate-admin-stage2.ts
|
|
*/
|
|
import { UserRole } from '@prisma/client';
|
|
import { createApp } from '../src/app.ts';
|
|
import { prisma } from '../src/lib/prisma.ts';
|
|
import { AUDIT_ACTIONS } from '../src/modules/admin/auditActions.ts';
|
|
|
|
const app = createApp();
|
|
const stamp = Date.now();
|
|
const adminEmail = `stage2-admin-${stamp}@example.com`;
|
|
const targetEmail = `stage2-target-${stamp}@example.com`;
|
|
const password = 'password12';
|
|
const createdIds: bigint[] = [];
|
|
|
|
async function request(path: string, init: RequestInit = {}) {
|
|
const headers = new Headers(init.headers);
|
|
if (init.body && !headers.has('Content-Type')) {
|
|
headers.set('Content-Type', 'application/json');
|
|
}
|
|
const res = await app.fetch(new Request(`http://localhost${path}`, { ...init, headers }));
|
|
const raw = await res.text();
|
|
let body: Record<string, unknown> = {};
|
|
try {
|
|
body = JSON.parse(raw) as Record<string, unknown>;
|
|
} catch {
|
|
body = {};
|
|
}
|
|
return { status: res.status, body, raw };
|
|
}
|
|
|
|
function assert(cond: unknown, message: string): asserts cond {
|
|
if (!cond) throw new Error(message);
|
|
}
|
|
|
|
try {
|
|
const regTarget = await request('/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email: targetEmail, password }),
|
|
});
|
|
assert(regTarget.status === 201, `target register failed: ${regTarget.status}`);
|
|
const targetExt = ((regTarget.body.data as { user: { id: string } }).user.id);
|
|
const targetUser = await prisma.user.findUniqueOrThrow({ where: { externalId: targetExt } });
|
|
createdIds.push(targetUser.id);
|
|
|
|
const regAdmin = await request('/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email: adminEmail, password }),
|
|
});
|
|
assert(regAdmin.status === 201, `admin register failed: ${regAdmin.status}`);
|
|
const adminExt = ((regAdmin.body.data as { user: { id: string } }).user.id);
|
|
const adminUser = await prisma.user.findUniqueOrThrow({ where: { externalId: adminExt } });
|
|
createdIds.push(adminUser.id);
|
|
await prisma.user.update({ where: { id: adminUser.id }, data: { role: UserRole.admin } });
|
|
|
|
const login = await request('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email: adminEmail, password }),
|
|
});
|
|
assert(login.status === 200, `admin login failed: ${login.status}`);
|
|
const token = (login.body.data as { sessionToken: string }).sessionToken;
|
|
assert(token && !/passwordHash|tokenHash/i.test(login.raw), 'login leaked secrets');
|
|
|
|
const auth = { Authorization: `Bearer ${token}` };
|
|
|
|
const unauth = await request('/admin/stats');
|
|
assert(unauth.status === 401, `expected 401, got ${unauth.status}`);
|
|
|
|
const stats = await request('/admin/stats', { headers: auth });
|
|
assert(stats.status === 200, `stats expected 200, got ${stats.status}`);
|
|
assert(!/passwordHash|tokenHash|sessionToken/i.test(stats.raw), 'stats leaked secrets');
|
|
const statsData = stats.body.data as Record<string, number>;
|
|
assert(typeof statsData.totalUsers === 'number', 'missing totalUsers');
|
|
|
|
const users = await request('/admin/users?page=1&pageSize=5&sortBy=createdAt&sortDir=desc', {
|
|
headers: auth,
|
|
});
|
|
assert(users.status === 200, `users expected 200, got ${users.status}`);
|
|
assert(!/passwordHash|tokenHash/i.test(users.raw), 'users leaked secrets');
|
|
|
|
const byEmail = await request(`/admin/users?q=${encodeURIComponent(targetEmail)}`, {
|
|
headers: auth,
|
|
});
|
|
assert(byEmail.status === 200, 'email search failed');
|
|
const emailItems = (byEmail.body.data as { items: Array<{ id: string; email: string | null }> })
|
|
.items;
|
|
assert(
|
|
emailItems.some((i) => i.email === targetEmail),
|
|
'email search missed target',
|
|
);
|
|
|
|
const byId = await request(`/admin/users?q=${encodeURIComponent(targetExt)}`, { headers: auth });
|
|
assert(byId.status === 200, 'externalId search failed');
|
|
const idItems = (byId.body.data as { items: Array<{ id: string }> }).items;
|
|
assert(idItems.some((i) => i.id === targetExt), 'externalId search missed target');
|
|
|
|
const page2 = await request('/admin/users?page=2&pageSize=1&sortBy=role&sortDir=asc', {
|
|
headers: auth,
|
|
});
|
|
assert(page2.status === 200, 'pagination/sort failed');
|
|
|
|
const badSort = await request('/admin/users?sortBy=notAWhitelistedField', { headers: auth });
|
|
assert(badSort.status === 422, `invalid sort expected 422, got ${badSort.status}`);
|
|
|
|
const relogin = await request('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email: adminEmail, password }),
|
|
});
|
|
assert(relogin.status === 200, 're-login failed');
|
|
const token2 = (relogin.body.data as { sessionToken: string; user: { role: string } }).sessionToken;
|
|
const role2 = (relogin.body.data as { user: { role: string } }).user.role;
|
|
assert(role2 === 'admin', 're-login role is not admin');
|
|
const stats2 = await request('/admin/stats', { headers: { Authorization: `Bearer ${token2}` } });
|
|
assert(stats2.status === 200, 'stats after re-login failed');
|
|
|
|
const auditStats = await prisma.auditLog.count({
|
|
where: { actorUserId: adminUser.id, action: AUDIT_ACTIONS.ADMIN_VIEW_STATS },
|
|
});
|
|
const auditUsers = await prisma.auditLog.count({
|
|
where: { actorUserId: adminUser.id, action: AUDIT_ACTIONS.ADMIN_VIEW_USERS },
|
|
});
|
|
assert(auditStats >= 2, `expected admin_view_stats audits, got ${auditStats}`);
|
|
assert(auditUsers >= 1, `expected admin_view_users audits, got ${auditUsers}`);
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
database: process.env.DATABASE_URL?.replace(/:[^:@/]+@/, ':***@'),
|
|
adminEmail,
|
|
targetEmail,
|
|
checks: [
|
|
'401 unauthenticated',
|
|
'200 stats',
|
|
'200 users',
|
|
'email search',
|
|
'externalId search',
|
|
'pagination+sort',
|
|
'422 invalid sort',
|
|
're-login + stats',
|
|
'audit logs',
|
|
'no secret leakage',
|
|
],
|
|
auditStats,
|
|
auditUsers,
|
|
totalUsersFromStats: statsData.totalUsers,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} catch (err) {
|
|
console.error(err instanceof Error ? err.message : err);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (createdIds.length > 0) {
|
|
await prisma.auditLog.deleteMany({
|
|
where: {
|
|
OR: [{ actorUserId: { in: createdIds } }, { targetUserId: { in: createdIds } }],
|
|
},
|
|
});
|
|
await prisma.user.deleteMany({ where: { id: { in: createdIds } } });
|
|
}
|
|
await prisma.$disconnect();
|
|
}
|