Superadmin impersonation, hardened auth/upload paths, dependency updates
- Add superadmin impersonation: sessions.impersonated_by (migration 0003) is stamped onto audit rows so impersonated actions are attributable, with a persistent ImpersonationBanner in the app shell. - Harden auth and upload handling across routes (safe redirect targets, filename sanitization, checkout grant handling). - Update dependencies: Sentry 8 -> 10, @fastify/static 8 -> 10, react-router-dom 6.30.6; add find-my-way / fast-uri overrides. - Add tests for safe-next, checkout-grant, and upload-filename. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4a1122a7c9
commit
eb36b81dc9
@@ -266,7 +266,9 @@ export async function adminRoutes(app: FastifyInstance) {
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Impersonate: end the current session, start a new one for the target user.
|
||||
// Impersonate: end the current session, start a short-lived one for the target user that is
|
||||
// permanently stamped with the acting admin's id. Every audit row written from that session
|
||||
// carries `impersonatedBy`, so support activity can never be mistaken for the customer's own.
|
||||
app.post('/api/admin/users/:id/impersonate', async (req, reply) => {
|
||||
const { id } = idParam.parse(req.params);
|
||||
const db = getDb();
|
||||
@@ -275,7 +277,11 @@ export async function adminRoutes(app: FastifyInstance) {
|
||||
if (!target) return reply.code(404).send({ error: 'not_found' });
|
||||
if (target.isSuspended) return reply.code(409).send({ error: 'target_suspended' });
|
||||
if (target.id === req.user!.id) return reply.code(409).send({ error: 'cannot_impersonate_self' });
|
||||
// Never let one platform admin borrow another's identity — that would launder an action
|
||||
// between two accounts that both hold full platform authority.
|
||||
if (target.isSuperadmin) return reply.code(409).send({ error: 'cannot_impersonate_superadmin' });
|
||||
|
||||
const adminId = req.user!.id;
|
||||
const oldToken = req.cookies?.[SESSION_COOKIE];
|
||||
if (oldToken) await destroySession(oldToken);
|
||||
|
||||
@@ -283,19 +289,24 @@ export async function adminRoutes(app: FastifyInstance) {
|
||||
userId: target.id,
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'] ?? null,
|
||||
impersonatedBy: adminId,
|
||||
});
|
||||
app.setSessionCookie(reply, token, expiresAt);
|
||||
app.setCsrfCookie(reply, generateCsrfToken());
|
||||
|
||||
await logAudit({
|
||||
userId: req.user!.id,
|
||||
userId: adminId,
|
||||
firmId: target.firmId,
|
||||
action: 'admin.impersonate',
|
||||
meta: { targetUserId: target.id, targetEmail: target.email },
|
||||
action: 'admin.impersonate.start',
|
||||
meta: { targetUserId: target.id, targetEmail: target.email, expiresAt: expiresAt.toISOString() },
|
||||
ip: req.ip,
|
||||
});
|
||||
|
||||
return { ok: true, impersonating: { id: target.id, email: target.email, firmId: target.firmId } };
|
||||
return {
|
||||
ok: true,
|
||||
impersonating: { id: target.id, email: target.email, firmId: target.firmId },
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
// ─────────────────────────── Contact inbox ───────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user