Admins can now surface dismissed alert types without waiting for the underlying data to change. Adds a collapsible "Restore dismissed" section below the active alerts with per-type checkboxes. - ALERT_METADATA map in adminAlerts.js as the single source of truth for slug → title/severity; detectors refactored to reference it - GET /api/admin/alerts/dismissed returns this admin's dismissals joined with metadata (title, severity, dismissedAt) - POST /api/admin/alerts/restore deletes dismissals by alertType[], returns the deleted count - AdminAlertsPanel fetches both active + dismissed; stays visible when either is non-empty; checkboxes + "Restore selected" button - adminAlertRestoreSchema validates the POST body against the enum - Auth guards test covers both new routes
107 lines
2.8 KiB
JavaScript
107 lines
2.8 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
|
|
const adminDir = resolve(import.meta.dirname, '../../../server/api/admin')
|
|
|
|
// All admin routes grouped by directory
|
|
const adminRoutes = {
|
|
'admin/': [
|
|
'dashboard.get.js',
|
|
'events.get.js',
|
|
'events.post.js',
|
|
'members.get.js',
|
|
'members.post.js',
|
|
'series.get.js',
|
|
'series.post.js',
|
|
'series.put.js'
|
|
],
|
|
'admin/events/': [
|
|
'events/[id].delete.js',
|
|
'events/[id].get.js',
|
|
'events/[id].put.js'
|
|
],
|
|
'admin/members/': [
|
|
'members/[id].put.js',
|
|
'members/[id]/role.patch.js',
|
|
'members/import.post.js',
|
|
'members/invite.post.js'
|
|
],
|
|
'admin/series/': [
|
|
'series/[id].delete.js',
|
|
'series/[id].put.js',
|
|
'series/tickets.put.js'
|
|
],
|
|
'admin/pre-registrants/': [
|
|
'pre-registrants/index.get.js',
|
|
'pre-registrants/[id].get.js',
|
|
'pre-registrants/[id].put.js',
|
|
'pre-registrants/bulk-status.patch.js',
|
|
'pre-registrants/invite.post.js',
|
|
'pre-registrants/stats.get.js'
|
|
],
|
|
'admin/alerts/': [
|
|
'alerts/index.get.js',
|
|
'alerts/dismiss.post.js',
|
|
'alerts/dismissed.get.js',
|
|
'alerts/restore.post.js'
|
|
]
|
|
}
|
|
|
|
// Business logic markers that must appear after requireAdmin
|
|
const businessLogicPatterns = [
|
|
'readBody(event)',
|
|
'validateBody(event',
|
|
'fetch(',
|
|
'connectDB()',
|
|
'Member.find',
|
|
'Member.findOne',
|
|
'Member.findById',
|
|
'Member.countDocuments',
|
|
'Event.find',
|
|
'Event.findOne',
|
|
'Event.findById',
|
|
'Event.countDocuments',
|
|
'Series.find',
|
|
'Series.findOne',
|
|
'Series.findById',
|
|
'PreRegistration.find',
|
|
'PreRegistration.findById',
|
|
'PreRegistration.aggregate',
|
|
'PreRegistration.updateMany',
|
|
'computeAllAlerts(',
|
|
'AdminAlertDismissal.findOneAndUpdate',
|
|
'AdminAlertDismissal.find',
|
|
'AdminAlertDismissal.deleteMany'
|
|
]
|
|
|
|
describe('Admin endpoint auth guards', () => {
|
|
for (const [group, files] of Object.entries(adminRoutes)) {
|
|
describe(group, () => {
|
|
for (const file of files) {
|
|
describe(file, () => {
|
|
const source = readFileSync(resolve(adminDir, file), 'utf-8')
|
|
|
|
it('calls requireAdmin', () => {
|
|
expect(source).toContain('requireAdmin(event)')
|
|
})
|
|
|
|
it('calls requireAdmin before any business logic', () => {
|
|
const adminIndex = source.indexOf('requireAdmin(event)')
|
|
expect(adminIndex).toBeGreaterThan(-1)
|
|
|
|
for (const pattern of businessLogicPatterns) {
|
|
const patternIndex = source.indexOf(pattern)
|
|
if (patternIndex > -1) {
|
|
expect(
|
|
adminIndex,
|
|
`requireAdmin must appear before ${pattern} in ${file}`
|
|
).toBeLessThan(patternIndex)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|