Six site improvements picked from a "what could be better" review, plus a Jest test suite covering the two areas with the trickiest money-handling history in this project (early-bird pricing tranches, donation-leg accounting): - "Add to calendar" .ics download on event pages and in confirmation emails - sitemap.xml, robots.txt, and Open Graph/Twitter metadata for public pages - Sentry error monitoring (backend + frontend), a no-op until SENTRY_DSN is set - Nightly local pg_dump backups with a Site Settings tab to browse/trigger/download - Admin audit trail for refunds, donations, manual registrations, event and settings changes, and staff-initiated cancellations - Jest tests reproducing and guarding against the 1.8.0 tranche-pricing bug and the 1.4.2 donation-balance-inflation bug Wallet passes (Google/Apple) were scoped out of this round — Apple Wallet needs a paid Apple Developer account the project doesn't have yet, and the user preferred shipping both together later rather than Google alone now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
1.7 KiB
JavaScript
53 lines
1.7 KiB
JavaScript
const prisma = require('../config/db');
|
|
|
|
// Fire-and-forget by design — a logging failure must never break the underlying admin
|
|
// action, so this swallows its own errors rather than propagating them to the caller
|
|
// (same posture as logSecurityEvent).
|
|
async function logAdminAction({ actorId, actorRole, action, targetType, targetId, metadata, ip }) {
|
|
try {
|
|
await prisma.adminAuditLog.create({
|
|
data: {
|
|
actorId: actorId || null,
|
|
actorRole,
|
|
action,
|
|
targetType,
|
|
targetId: targetId || null,
|
|
metadata: metadata || undefined,
|
|
ip: ip || null,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error('Failed to log admin action:', e?.message);
|
|
}
|
|
}
|
|
|
|
// Paginated listing for the admin audit-log page, with optional actor/action/date filters.
|
|
async function getAdminAuditLog({ page = 1, limit = 50, actorId, action, from, to } = {}) {
|
|
const where = {};
|
|
if (actorId) where.actorId = actorId;
|
|
if (action) where.action = action;
|
|
if (from || to) {
|
|
where.createdAt = {};
|
|
if (from) where.createdAt.gte = new Date(from);
|
|
if (to) where.createdAt.lte = new Date(to);
|
|
}
|
|
|
|
const take = Math.min(Math.max(Number(limit) || 50, 1), 200);
|
|
const skip = (Math.max(Number(page) || 1, 1) - 1) * take;
|
|
|
|
const [rows, total] = await Promise.all([
|
|
prisma.adminAuditLog.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
take,
|
|
skip,
|
|
include: { actor: { select: { id: true, name: true, email: true } } },
|
|
}),
|
|
prisma.adminAuditLog.count({ where }),
|
|
]);
|
|
|
|
return { rows, total, page: Math.max(Number(page) || 1, 1), limit: take };
|
|
}
|
|
|
|
module.exports = { logAdminAction, getAdminAuditLog };
|