add features

This commit is contained in:
Matthias Hochmeister
2026-02-27 19:50:14 +01:00
parent c5e8337a69
commit 620bacc6b5
46 changed files with 14095 additions and 1 deletions

View File

@@ -0,0 +1,169 @@
/**
* Admin API Routes — Audit Log
*
* GET /api/admin/audit-log — paginated, filtered list
* GET /api/admin/audit-log/export — CSV download of filtered results
*
* Both endpoints require authentication + admin:access permission.
*
* Register in app.ts:
* import adminRoutes from './routes/admin.routes';
* app.use('/api/admin', adminRoutes);
*/
import { Router, Request, Response } from 'express';
import { z } from 'zod';
import { authenticate } from '../middleware/auth.middleware';
import { requirePermission } from '../middleware/rbac.middleware';
import { auditExport } from '../middleware/audit.middleware';
import auditService, { AuditAction, AuditResourceType, AuditFilters } from '../services/audit.service';
import logger from '../utils/logger';
const router = Router();
// ---------------------------------------------------------------------------
// Input validation schemas (Zod)
// ---------------------------------------------------------------------------
const auditQuerySchema = z.object({
userId: z.string().uuid().optional(),
action: z.union([
z.nativeEnum(Object.fromEntries(
Object.entries(AuditAction).map(([k, v]) => [k, v])
) as Record<string, string>),
z.array(z.string()),
]).optional(),
resourceType: z.union([
z.string(),
z.array(z.string()),
]).optional(),
resourceId: z.string().optional(),
dateFrom: z.string().datetime({ offset: true }).optional(),
dateTo: z.string().datetime({ offset: true }).optional(),
page: z.coerce.number().int().positive().default(1),
pageSize: z.coerce.number().int().min(1).max(200).default(25),
});
// ---------------------------------------------------------------------------
// Helper — parse and validate query string
// ---------------------------------------------------------------------------
function parseAuditQuery(query: Record<string, unknown>): AuditFilters {
const parsed = auditQuerySchema.parse(query);
// Normalise action to array of AuditAction
const actions = parsed.action
? (Array.isArray(parsed.action) ? parsed.action : [parsed.action]) as AuditAction[]
: undefined;
// Normalise resourceType to array of AuditResourceType
const resourceTypes = parsed.resourceType
? (Array.isArray(parsed.resourceType)
? parsed.resourceType
: [parsed.resourceType]) as AuditResourceType[]
: undefined;
return {
userId: parsed.userId,
action: actions && actions.length === 1 ? actions[0] : actions,
resourceType: resourceTypes && resourceTypes.length === 1
? resourceTypes[0]
: resourceTypes,
resourceId: parsed.resourceId,
dateFrom: parsed.dateFrom ? new Date(parsed.dateFrom) : undefined,
dateTo: parsed.dateTo ? new Date(parsed.dateTo) : undefined,
page: parsed.page,
pageSize: parsed.pageSize,
};
}
// ---------------------------------------------------------------------------
// GET /api/admin/audit-log
// ---------------------------------------------------------------------------
router.get(
'/audit-log',
authenticate,
requirePermission('admin:access'),
async (req: Request, res: Response): Promise<void> => {
try {
const filters = parseAuditQuery(req.query as Record<string, unknown>);
const result = await auditService.getAuditLog(filters);
res.status(200).json({
success: true,
data: result,
});
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({
success: false,
message: 'Invalid query parameters',
errors: error.errors,
});
return;
}
logger.error('Failed to fetch audit log', { error });
res.status(500).json({
success: false,
message: 'Failed to fetch audit log',
});
}
}
);
// ---------------------------------------------------------------------------
// GET /api/admin/audit-log/export
// ---------------------------------------------------------------------------
router.get(
'/audit-log/export',
authenticate,
requirePermission('admin:access'),
async (req: Request, res: Response): Promise<void> => {
try {
// For CSV exports we fetch up to 10,000 rows (no pagination).
const filters = parseAuditQuery(req.query as Record<string, unknown>);
const exportFilters: AuditFilters = {
...filters,
page: 1,
pageSize: 10_000,
};
// Audit the export action itself before streaming the response
auditExport(req, AuditResourceType.SYSTEM, {
export_format: 'csv',
filters: JSON.stringify(exportFilters),
});
const result = await auditService.getAuditLog(exportFilters);
const csv = auditService.entriesToCsv(result.entries);
const filename = `audit_log_${new Date().toISOString().replace(/[:.]/g, '-')}.csv`;
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
// Add BOM for Excel UTF-8 compatibility
res.send('\uFEFF' + csv);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({
success: false,
message: 'Invalid query parameters',
errors: error.errors,
});
return;
}
logger.error('Failed to export audit log', { error });
res.status(500).json({
success: false,
message: 'Failed to export audit log',
});
}
}
);
export default router;