feat(03-01): create admin routes and mount at /admin
- admin.ts: four endpoints (GET /health, GET /analytics, GET /alerts, POST /alerts/:id/acknowledge)
- Auth chain: addCorrelationId + verifyFirebaseToken + requireAdminEmail (router-level)
- Health: queries all four service names matching healthProbeService output
- Analytics: validates range format (/^\d+[hd]$/) then delegates to getAnalyticsSummary()
- Alerts: findActive() returns all active alerts; acknowledge returns 404 on not-found
- Response envelope: { success, data, correlationId } matching codebase pattern
- index.ts: mounts admin router at /admin alongside existing routes
This commit is contained in:
@@ -12,6 +12,7 @@ import documentRoutes from './routes/documents';
|
||||
import vectorRoutes from './routes/vector';
|
||||
import monitoringRoutes from './routes/monitoring';
|
||||
import auditRoutes from './routes/documentAudit';
|
||||
import adminRoutes from './routes/admin';
|
||||
import { jobQueueService } from './services/jobQueueService';
|
||||
|
||||
import { errorHandler, correlationIdMiddleware } from './middleware/errorHandler';
|
||||
@@ -180,6 +181,7 @@ app.use('/documents', documentRoutes);
|
||||
app.use('/vector', vectorRoutes);
|
||||
app.use('/monitoring', monitoringRoutes);
|
||||
app.use('/api/audit', auditRoutes);
|
||||
app.use('/admin', adminRoutes);
|
||||
|
||||
|
||||
import { onRequest } from 'firebase-functions/v2/https';
|
||||
|
||||
152
backend/src/routes/admin.ts
Normal file
152
backend/src/routes/admin.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Router, Response } from 'express';
|
||||
import { verifyFirebaseToken, FirebaseAuthenticatedRequest } from '../middleware/firebaseAuth';
|
||||
import { requireAdminEmail } from '../middleware/requireAdmin';
|
||||
import { addCorrelationId } from '../middleware/validation';
|
||||
import { HealthCheckModel } from '../models/HealthCheckModel';
|
||||
import { AlertEventModel } from '../models/AlertEventModel';
|
||||
import { getAnalyticsSummary } from '../services/analyticsService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Service names must match what healthProbeService writes
|
||||
const SERVICE_NAMES = ['document_ai', 'llm_api', 'supabase', 'firebase_auth'] as const;
|
||||
|
||||
// Apply auth + admin check + correlation ID to all admin routes
|
||||
router.use(addCorrelationId);
|
||||
router.use(verifyFirebaseToken);
|
||||
router.use(requireAdminEmail);
|
||||
|
||||
/**
|
||||
* GET /admin/health
|
||||
* Returns latest health check for all four monitored services.
|
||||
*/
|
||||
router.get('/health', async (req: FirebaseAuthenticatedRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
SERVICE_NAMES.map(name => HealthCheckModel.findLatestByService(name))
|
||||
);
|
||||
|
||||
const data = results.map((record, index) => {
|
||||
const serviceName = SERVICE_NAMES[index]!;
|
||||
if (!record) {
|
||||
return {
|
||||
service: serviceName,
|
||||
status: 'unknown' as const,
|
||||
checkedAt: null,
|
||||
latencyMs: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
service: record.service_name,
|
||||
status: record.status,
|
||||
checkedAt: record.checked_at,
|
||||
latencyMs: record.latency_ms,
|
||||
errorMessage: record.error_message,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ success: true, data, correlationId: req.correlationId });
|
||||
} catch (error) {
|
||||
logger.error('admin: GET /health failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve health status',
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /admin/analytics
|
||||
* Returns document processing summary for a configurable time range.
|
||||
* Query param: ?range=24h (default) or any NNh / NNd format.
|
||||
*/
|
||||
router.get('/analytics', async (req: FirebaseAuthenticatedRequest, res: Response): Promise<void> => {
|
||||
const range = (req.query['range'] as string) ?? '24h';
|
||||
|
||||
if (!/^\d+[hd]$/.test(range)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid range parameter. Use format: 24h, 7d, etc.',
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const summary = await getAnalyticsSummary(range);
|
||||
res.json({ success: true, data: summary, correlationId: req.correlationId });
|
||||
} catch (error) {
|
||||
logger.error('admin: GET /analytics failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
range,
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve analytics summary',
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /admin/alerts
|
||||
* Returns all active (unacknowledged, unresolved) alert events.
|
||||
*/
|
||||
router.get('/alerts', async (req: FirebaseAuthenticatedRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const alerts = await AlertEventModel.findActive();
|
||||
res.json({ success: true, data: alerts, correlationId: req.correlationId });
|
||||
} catch (error) {
|
||||
logger.error('admin: GET /alerts failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve active alerts',
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /admin/alerts/:id/acknowledge
|
||||
* Marks an alert event as acknowledged.
|
||||
*/
|
||||
router.post('/alerts/:id/acknowledge', async (req: FirebaseAuthenticatedRequest, res: Response): Promise<void> => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const updatedAlert = await AlertEventModel.acknowledge(id!);
|
||||
res.json({ success: true, data: updatedAlert, correlationId: req.correlationId });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.toLowerCase().includes('not found')) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Alert not found: ${id}`,
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
logger.error('admin: POST /alerts/:id/acknowledge failed', {
|
||||
error: message,
|
||||
id,
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to acknowledge alert',
|
||||
correlationId: req.correlationId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user