feat(phase-2): workstream 6 — Web Dashboard UI

- dashboard/: Vite 5 + React 18 + TypeScript strict SPA
  - Auth: sessionStorage credentials, TokenManager validation, AuthProvider context
  - Pages: Login, Agents (search + filter), AgentDetail (suspend/reactivate),
    Credentials (generate/rotate/revoke, new secret shown once),
    AuditLog (filters + pagination), Health (PG + Redis status, 30s refresh)
  - Components: Button, Badge, ConfirmDialog, AppShell, RequireAuth
  - All destructive actions gated by ConfirmDialog
  - Zero dangerouslySetInnerHTML; sessionStorage only (OWASP compliant)
- src/routes/health.ts: unauthenticated GET /health — PG + Redis connectivity
- src/app.ts: health route + dashboard/dist/ served at /dashboard with SPA fallback
- 6 new health route tests; 308/308 unit tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-03-28 23:19:18 +00:00
parent 7328a61c44
commit 7d6e248a14
32 changed files with 4858 additions and 13 deletions

View File

@@ -31,11 +31,13 @@ import { createAgentsRouter } from './routes/agents.js';
import { createTokenRouter } from './routes/token.js';
import { createCredentialsRouter } from './routes/credentials.js';
import { createAuditRouter } from './routes/audit.js';
import { createHealthRouter } from './routes/health.js';
import { errorHandler } from './middleware/errorHandler.js';
import { createOpaMiddleware } from './middleware/opa.js';
import { createVaultClientFromEnv } from './vault/VaultClient.js';
import { RedisClientType } from 'redis';
import path from 'path';
/**
* Creates and returns a configured Express application.
@@ -139,6 +141,9 @@ export async function createApp(): Promise<Application> {
// ────────────────────────────────────────────────────────────────
const API_BASE = '/api/v1';
// Health check — unauthenticated, no OPA
app.use('/health', createHealthRouter(pool, redis as RedisClientType));
app.use(`${API_BASE}/agents`, createAgentsRouter(agentController, opaMiddleware));
app.use(
`${API_BASE}/agents/:agentId/credentials`,
@@ -147,6 +152,20 @@ export async function createApp(): Promise<Application> {
app.use(`${API_BASE}/token`, createTokenRouter(tokenController, opaMiddleware));
app.use(`${API_BASE}/audit`, createAuditRouter(auditController, opaMiddleware));
// ────────────────────────────────────────────────────────────────
// Dashboard static assets (served from dashboard/dist/)
// Placed after API routes so API routes take precedence.
// __dirname is available because the project compiles to CommonJS.
// ────────────────────────────────────────────────────────────────
const dashboardDist = path.resolve(__dirname, '../../dashboard/dist');
app.use('/dashboard', express.static(dashboardDist));
// SPA fallback — serve index.html for all /dashboard/* routes not matching a static file
app.get('/dashboard/*', (_req, res) => {
res.sendFile(path.join(dashboardDist, 'index.html'));
});
// ────────────────────────────────────────────────────────────────
// Global error handler (must be last)
// ────────────────────────────────────────────────────────────────