Files
pwa/backend/scripts/validate-admin-stage2.ts
ar.badieiandCursor c6243caa35 Add read-only Noya admin panel with assignments and UX polish.
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>
2026-09-21 08:00:24 +03:30

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