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:
admin
2026-02-24 15:44:41 -05:00
parent dabd4a5ecf
commit 4169a3731f
2 changed files with 154 additions and 0 deletions

View File

@@ -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
View 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;