mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-26 02:06:49 +00:00
50e64b058b
First slice of Phase 4 (route extraction). Pulls 8 well-tested, mostly
independent route groups out of index.ts into focused Router files. No
behavior change; every handler body moves verbatim.
New route files under backend/src/routes/:
- meta.ts /api/health, /api/meta (mounted before authGate)
- license.ts /api/license/* + /api/system/update,
exports scheduleLocalUpdate for the fleet route
- permissions.ts /api/permissions/me
- convert.ts POST /api/convert
- alerts.ts /api/alerts/*
- labels.ts /api/labels/* + PUT /api/stacks/:name/labels
(exported as stackLabelsRouter)
- apiTokens.ts /api/api-tokens/*
- auditLog.ts /api/audit-log/*
Shared helper lifts:
- helpers/cacheInvalidation.ts: invalidateNodeCaches()
- middleware/tierGates.ts: requireBody (was inline in index.ts)
- utils/errors.ts: isSqliteUniqueViolation (was inline in index.ts)
- middleware/apiTokenScope.ts: rejectApiTokenScope() helper (new)
- utils/csv.ts: escapeCsvField() (new)
index.ts drops from ~7520 to ~6775 lines and now mounts the routers right
after enforceApiTokenScope. The remote proxy and fleet/auth/webhooks/users
routes remain inline in index.ts pending later Phase 4 slices.
Code review fixes: rejectApiTokenScope helper replaces duplicated
`if (req.apiTokenScope) 403 SCOPE_DENIED` blocks in apiTokens.ts and
license.ts; escapeCsvField replaces the inline CSV escape in auditLog.ts.
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
import { Router, type Request, type Response } from 'express';
|
|
import { z } from 'zod';
|
|
import { DatabaseService } from '../services/DatabaseService';
|
|
import { authMiddleware } from '../middleware/auth';
|
|
import { requireAdmin } from '../middleware/tierGates';
|
|
|
|
const AlertCreateSchema = z.object({
|
|
stack_name: z.string().min(1).max(255),
|
|
metric: z.enum(['cpu_percent', 'memory_percent', 'memory_mb', 'net_rx', 'net_tx', 'restart_count']),
|
|
operator: z.enum(['>', '>=', '<', '<=', '==']),
|
|
threshold: z.number().min(0),
|
|
duration_mins: z.coerce.number().int().min(0).max(1440),
|
|
cooldown_mins: z.coerce.number().int().min(0).max(10080),
|
|
});
|
|
|
|
export const alertsRouter = Router();
|
|
|
|
alertsRouter.get('/', authMiddleware, async (req: Request, res: Response) => {
|
|
try {
|
|
let stackName = req.query.stackName as string | undefined;
|
|
if (Array.isArray(stackName)) stackName = stackName[0] as string;
|
|
|
|
const alerts = DatabaseService.getInstance().getStackAlerts(stackName);
|
|
res.json(alerts);
|
|
} catch {
|
|
res.status(500).json({ error: 'Failed to fetch alerts' });
|
|
}
|
|
});
|
|
|
|
alertsRouter.post('/', authMiddleware, async (req: Request, res: Response) => {
|
|
if (!requireAdmin(req, res)) return;
|
|
const parsed = AlertCreateSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
res.status(400).json({ error: 'Invalid alert data', details: parsed.error.flatten().fieldErrors });
|
|
return;
|
|
}
|
|
try {
|
|
const created = DatabaseService.getInstance().addStackAlert(parsed.data);
|
|
res.status(201).json(created);
|
|
} catch (error) {
|
|
console.error('Failed to add alert:', error);
|
|
res.status(500).json({ error: 'Failed to add alert' });
|
|
}
|
|
});
|
|
|
|
alertsRouter.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
|
|
if (!requireAdmin(req, res)) return;
|
|
try {
|
|
const id = parseInt(req.params.id as string, 10);
|
|
DatabaseService.getInstance().deleteStackAlert(id);
|
|
res.json({ success: true });
|
|
} catch {
|
|
res.status(500).json({ error: 'Failed to delete alert' });
|
|
}
|
|
});
|