feat(governance): Governance Engine MVP implementation
- Backend: - Migration 032: agent_gov_level, status, incidents, permissions tables - Domain types for governance layer - Permission Engine with all governance checks - Governance Service (promote/demote/roles) - Revocation Service (revoke/suspend/reinstate) - Audit Service (events filtering and stats) - Incidents Service (create/assign/escalate/resolve) - REST API routes for governance, audit, incidents - Frontend: - TypeScript types for governance - API clients for governance, audit, incidents - GovernanceLevelBadge component - CityGovernancePanel component - AuditDashboard component - IncidentsList component with detail modal Based on: Agent_Governance_Protocol_v1.md
This commit is contained in:
178
backend/http/audit.routes.ts
Normal file
178
backend/http/audit.routes.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Audit Routes
|
||||
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { auditService } from '../services/governance/audit.service';
|
||||
import { GovernanceEventType, GovernanceScope } from '../domain/governance/types';
|
||||
import { logger } from '../infra/logger/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/v1/audit/events
|
||||
* Get audit events with filters
|
||||
*/
|
||||
router.get('/events', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
eventType,
|
||||
actorId,
|
||||
targetId,
|
||||
scope,
|
||||
createdAtFrom,
|
||||
createdAtTo,
|
||||
limit,
|
||||
offset,
|
||||
} = req.query;
|
||||
|
||||
const result = await auditService.getEvents({
|
||||
eventType: eventType as GovernanceEventType | undefined,
|
||||
actorId: actorId as string | undefined,
|
||||
targetId: targetId as string | undefined,
|
||||
scope: scope as GovernanceScope | undefined,
|
||||
createdAtFrom: createdAtFrom ? new Date(createdAtFrom as string) : undefined,
|
||||
createdAtTo: createdAtTo ? new Date(createdAtTo as string) : undefined,
|
||||
limit: limit ? parseInt(limit as string, 10) : 50,
|
||||
offset: offset ? parseInt(offset as string, 10) : 0,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error getting audit events', error);
|
||||
res.status(500).json({ error: 'Failed to get audit events' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/audit/events/:id
|
||||
* Get single audit event
|
||||
*/
|
||||
router.get('/events/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const event = await auditService.getEvent(id);
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
logger.error('Error getting audit event', error);
|
||||
res.status(500).json({ error: 'Failed to get audit event' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/audit/actor/:actorId
|
||||
* Get events by actor
|
||||
*/
|
||||
router.get('/actor/:actorId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId } = req.params;
|
||||
const { limit } = req.query;
|
||||
|
||||
const events = await auditService.getEventsByActor(
|
||||
actorId,
|
||||
limit ? parseInt(limit as string, 10) : 50
|
||||
);
|
||||
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
logger.error('Error getting events by actor', error);
|
||||
res.status(500).json({ error: 'Failed to get events by actor' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/audit/target/:targetId
|
||||
* Get events by target
|
||||
*/
|
||||
router.get('/target/:targetId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { targetId } = req.params;
|
||||
const { limit } = req.query;
|
||||
|
||||
const events = await auditService.getEventsByTarget(
|
||||
targetId,
|
||||
limit ? parseInt(limit as string, 10) : 50
|
||||
);
|
||||
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
logger.error('Error getting events by target', error);
|
||||
res.status(500).json({ error: 'Failed to get events by target' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/audit/scope/:scope
|
||||
* Get events by scope
|
||||
*/
|
||||
router.get('/scope/:scope', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { scope } = req.params;
|
||||
const { limit } = req.query;
|
||||
|
||||
const events = await auditService.getEventsByScope(
|
||||
scope as GovernanceScope,
|
||||
limit ? parseInt(limit as string, 10) : 50
|
||||
);
|
||||
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
logger.error('Error getting events by scope', error);
|
||||
res.status(500).json({ error: 'Failed to get events by scope' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/audit/stats
|
||||
* Get event statistics
|
||||
*/
|
||||
router.get('/stats', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { fromDate, toDate } = req.query;
|
||||
|
||||
const stats = await auditService.getEventStats(
|
||||
fromDate ? new Date(fromDate as string) : undefined,
|
||||
toDate ? new Date(toDate as string) : undefined
|
||||
);
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
logger.error('Error getting audit stats', error);
|
||||
res.status(500).json({ error: 'Failed to get audit stats' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/audit/entity/:entityType/:entityId
|
||||
* Get governance history for specific entity
|
||||
*/
|
||||
router.get('/entity/:entityType/:entityId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { entityType, entityId } = req.params;
|
||||
const { limit } = req.query;
|
||||
|
||||
if (!['agent', 'microdao', 'district', 'node', 'room'].includes(entityType)) {
|
||||
return res.status(400).json({ error: 'Invalid entity type' });
|
||||
}
|
||||
|
||||
const events = await auditService.getEntityHistory(
|
||||
entityType as 'agent' | 'microdao' | 'district' | 'node' | 'room',
|
||||
entityId,
|
||||
limit ? parseInt(limit as string, 10) : 50
|
||||
);
|
||||
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
logger.error('Error getting entity history', error);
|
||||
res.status(500).json({ error: 'Failed to get entity history' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
386
backend/http/governance.routes.ts
Normal file
386
backend/http/governance.routes.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* Governance Routes
|
||||
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { governanceService } from '../services/governance/governance.service';
|
||||
import { revocationService } from '../services/governance/revocation.service';
|
||||
import { permissionEngine, buildContext } from '../services/governance/permissions';
|
||||
import { AgentGovLevel, GovernanceScope, RevocationType } from '../domain/governance/types';
|
||||
import { logger } from '../infra/logger/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ============================================================================
|
||||
// AGENT PROMOTION/DEMOTION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/v1/governance/agent/promote
|
||||
* Promote an agent to a higher level
|
||||
*/
|
||||
router.post('/agent/promote', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId, targetId, newLevel, scope, reason } = req.body;
|
||||
|
||||
if (!actorId || !targetId || !newLevel || !scope) {
|
||||
return res.status(400).json({ error: 'Missing required fields: actorId, targetId, newLevel, scope' });
|
||||
}
|
||||
|
||||
const result = await governanceService.promoteAgent({
|
||||
actorId,
|
||||
targetId,
|
||||
newLevel: newLevel as AgentGovLevel,
|
||||
scope: scope as GovernanceScope,
|
||||
reason,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(403).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: `Agent ${targetId} promoted to ${newLevel}` });
|
||||
} catch (error) {
|
||||
logger.error('Error promoting agent', error);
|
||||
res.status(500).json({ error: 'Failed to promote agent' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/governance/agent/demote
|
||||
* Demote an agent to a lower level
|
||||
*/
|
||||
router.post('/agent/demote', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId, targetId, newLevel, scope, reason } = req.body;
|
||||
|
||||
if (!actorId || !targetId || !newLevel || !scope) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const result = await governanceService.demoteAgent(
|
||||
actorId,
|
||||
targetId,
|
||||
newLevel as AgentGovLevel,
|
||||
scope as GovernanceScope,
|
||||
reason
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(403).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: `Agent ${targetId} demoted to ${newLevel}` });
|
||||
} catch (error) {
|
||||
logger.error('Error demoting agent', error);
|
||||
res.status(500).json({ error: 'Failed to demote agent' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// AGENT REVOCATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/v1/governance/agent/revoke
|
||||
* Revoke an agent
|
||||
*/
|
||||
router.post('/agent/revoke', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId, targetId, reason, scope, revocationType } = req.body;
|
||||
|
||||
if (!actorId || !targetId || !reason || !scope) {
|
||||
return res.status(400).json({ error: 'Missing required fields: actorId, targetId, reason, scope' });
|
||||
}
|
||||
|
||||
const result = await revocationService.revokeAgent({
|
||||
actorId,
|
||||
targetId,
|
||||
reason,
|
||||
scope: scope as GovernanceScope,
|
||||
revocationType: (revocationType || 'soft') as RevocationType,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(403).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
revocationId: result.revocationId,
|
||||
message: `Agent ${targetId} revoked`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error revoking agent', error);
|
||||
res.status(500).json({ error: 'Failed to revoke agent' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/governance/agent/suspend
|
||||
* Temporarily suspend an agent
|
||||
*/
|
||||
router.post('/agent/suspend', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId, targetId, reason, scope, durationHours } = req.body;
|
||||
|
||||
if (!actorId || !targetId || !reason || !scope) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const result = await revocationService.suspendAgent(
|
||||
actorId,
|
||||
targetId,
|
||||
reason,
|
||||
scope as GovernanceScope,
|
||||
durationHours
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(403).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: `Agent ${targetId} suspended` });
|
||||
} catch (error) {
|
||||
logger.error('Error suspending agent', error);
|
||||
res.status(500).json({ error: 'Failed to suspend agent' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/governance/agent/reinstate
|
||||
* Reinstate a revoked/suspended agent
|
||||
*/
|
||||
router.post('/agent/reinstate', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId, targetId, scope, reason } = req.body;
|
||||
|
||||
if (!actorId || !targetId || !scope) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const result = await revocationService.reinstateAgent(
|
||||
actorId,
|
||||
targetId,
|
||||
scope as GovernanceScope,
|
||||
reason
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(403).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: `Agent ${targetId} reinstated` });
|
||||
} catch (error) {
|
||||
logger.error('Error reinstating agent', error);
|
||||
res.status(500).json({ error: 'Failed to reinstate agent' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// AGENT ROLES & PERMISSIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* GET /api/v1/governance/agent/:id/roles
|
||||
* Get agent roles and permissions
|
||||
*/
|
||||
router.get('/agent/:id/roles', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const roles = await governanceService.getAgentRoles(id);
|
||||
res.json(roles);
|
||||
} catch (error) {
|
||||
logger.error('Error getting agent roles', error);
|
||||
res.status(500).json({ error: 'Failed to get agent roles' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/governance/agent/:id/permissions
|
||||
* Get agent permissions for a specific target
|
||||
*/
|
||||
router.get('/agent/:id/permissions', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { targetType, targetId, action } = req.query;
|
||||
|
||||
if (targetType && targetId && action) {
|
||||
// Get DAIS ID for agent
|
||||
const context = await buildContext(id, 'city');
|
||||
const hasPermission = await permissionEngine.hasExplicitPermission(
|
||||
context.actorDaisId,
|
||||
targetType as any,
|
||||
targetId as string,
|
||||
action as any
|
||||
);
|
||||
|
||||
return res.json({ hasPermission });
|
||||
}
|
||||
|
||||
// Return general permissions
|
||||
const level = await permissionEngine.getAgentLevel(id);
|
||||
const powers = permissionEngine.getPowersForLevel(level);
|
||||
|
||||
res.json({ level, powers });
|
||||
} catch (error) {
|
||||
logger.error('Error getting agent permissions', error);
|
||||
res.status(500).json({ error: 'Failed to get agent permissions' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// PERMISSION CHECKS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/v1/governance/check
|
||||
* Check if an agent can perform an action
|
||||
*/
|
||||
router.post('/check', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId, action, targetId, scope, roomType } = req.body;
|
||||
|
||||
if (!actorId || !action) {
|
||||
return res.status(400).json({ error: 'Missing required fields: actorId, action' });
|
||||
}
|
||||
|
||||
const context = await buildContext(actorId, (scope || 'city') as GovernanceScope);
|
||||
|
||||
let result;
|
||||
|
||||
switch (action) {
|
||||
case 'create_microdao':
|
||||
result = await permissionEngine.canCreateMicrodao(context);
|
||||
break;
|
||||
case 'create_district':
|
||||
result = await permissionEngine.canCreateDistrict(context);
|
||||
break;
|
||||
case 'register_node':
|
||||
result = await permissionEngine.canRegisterNode(context);
|
||||
break;
|
||||
case 'create_room':
|
||||
result = await permissionEngine.canCreateRoom(context, roomType || 'dao-room');
|
||||
break;
|
||||
case 'create_front_portal':
|
||||
result = await permissionEngine.canCreateFrontPortal(context, targetId);
|
||||
break;
|
||||
case 'moderate_room':
|
||||
result = await permissionEngine.canModerateRoom(context, targetId);
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: `Unknown action: ${action}` });
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error checking permission', error);
|
||||
res.status(500).json({ error: 'Failed to check permission' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// GOVERNANCE AGENTS LISTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* GET /api/v1/governance/agents/city
|
||||
* Get city governance agents
|
||||
*/
|
||||
router.get('/agents/city', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const agents = await governanceService.getCityGovernanceAgents();
|
||||
res.json(agents);
|
||||
} catch (error) {
|
||||
logger.error('Error getting city governance agents', error);
|
||||
res.status(500).json({ error: 'Failed to get city governance agents' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/governance/agents/district-leads
|
||||
* Get district lead agents
|
||||
*/
|
||||
router.get('/agents/district-leads', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { districtId } = req.query;
|
||||
const agents = await governanceService.getDistrictLeadAgents(districtId as string);
|
||||
res.json(agents);
|
||||
} catch (error) {
|
||||
logger.error('Error getting district lead agents', error);
|
||||
res.status(500).json({ error: 'Failed to get district lead agents' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/governance/agents/by-level/:level
|
||||
* Get agents by governance level
|
||||
*/
|
||||
router.get('/agents/by-level/:level', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { level } = req.params;
|
||||
const { limit } = req.query;
|
||||
|
||||
const agents = await governanceService.getAgentsByLevel(
|
||||
level as AgentGovLevel,
|
||||
limit ? parseInt(limit as string, 10) : 50
|
||||
);
|
||||
|
||||
res.json(agents);
|
||||
} catch (error) {
|
||||
logger.error('Error getting agents by level', error);
|
||||
res.status(500).json({ error: 'Failed to get agents by level' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// DAIS KEY REVOCATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/v1/governance/dais/keys/revoke
|
||||
* Revoke DAIS keys
|
||||
*/
|
||||
router.post('/dais/keys/revoke', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { actorId, daisId, reason } = req.body;
|
||||
|
||||
if (!actorId || !daisId || !reason) {
|
||||
return res.status(400).json({ error: 'Missing required fields: actorId, daisId, reason' });
|
||||
}
|
||||
|
||||
const result = await revocationService.revokeDaisKeys(actorId, daisId, reason);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(403).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
keysRevoked: result.keysRevoked,
|
||||
message: `${result.keysRevoked} keys revoked for DAIS ${daisId}`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error revoking DAIS keys', error);
|
||||
res.status(500).json({ error: 'Failed to revoke DAIS keys' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/governance/agent/:id/revocations
|
||||
* Get revocation history for an agent
|
||||
*/
|
||||
router.get('/agent/:id/revocations', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const history = await revocationService.getRevocationHistory(id);
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
logger.error('Error getting revocation history', error);
|
||||
res.status(500).json({ error: 'Failed to get revocation history' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
293
backend/http/incidents.routes.ts
Normal file
293
backend/http/incidents.routes.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Incidents Routes
|
||||
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { incidentsService } from '../services/governance/incidents.service';
|
||||
import {
|
||||
IncidentStatus,
|
||||
IncidentPriority,
|
||||
EscalationLevel,
|
||||
TargetScopeType,
|
||||
} from '../domain/governance/types';
|
||||
import { logger } from '../infra/logger/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* POST /api/v1/incidents
|
||||
* Create a new incident
|
||||
*/
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
createdByDaisId,
|
||||
targetScopeType,
|
||||
targetScopeId,
|
||||
priority,
|
||||
title,
|
||||
description,
|
||||
metadata,
|
||||
} = req.body;
|
||||
|
||||
if (!createdByDaisId || !targetScopeType || !targetScopeId || !title) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing required fields: createdByDaisId, targetScopeType, targetScopeId, title',
|
||||
});
|
||||
}
|
||||
|
||||
const incident = await incidentsService.createIncident({
|
||||
createdByDaisId,
|
||||
targetScopeType: targetScopeType as TargetScopeType,
|
||||
targetScopeId,
|
||||
priority: priority as IncidentPriority | undefined,
|
||||
title,
|
||||
description,
|
||||
metadata,
|
||||
});
|
||||
|
||||
res.status(201).json(incident);
|
||||
} catch (error) {
|
||||
logger.error('Error creating incident', error);
|
||||
res.status(500).json({ error: 'Failed to create incident' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/incidents
|
||||
* List incidents with filters
|
||||
*/
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
status,
|
||||
priority,
|
||||
escalationLevel,
|
||||
targetScopeType,
|
||||
targetScopeId,
|
||||
assignedToDaisId,
|
||||
limit,
|
||||
offset,
|
||||
} = req.query;
|
||||
|
||||
const result = await incidentsService.listIncidents({
|
||||
status: status as IncidentStatus | undefined,
|
||||
priority: priority as IncidentPriority | undefined,
|
||||
escalationLevel: escalationLevel as EscalationLevel | undefined,
|
||||
targetScopeType: targetScopeType as TargetScopeType | undefined,
|
||||
targetScopeId: targetScopeId as string | undefined,
|
||||
assignedToDaisId: assignedToDaisId as string | undefined,
|
||||
limit: limit ? parseInt(limit as string, 10) : 50,
|
||||
offset: offset ? parseInt(offset as string, 10) : 0,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error listing incidents', error);
|
||||
res.status(500).json({ error: 'Failed to list incidents' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/incidents/count
|
||||
* Get open incidents count by level
|
||||
*/
|
||||
router.get('/count', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const counts = await incidentsService.getOpenIncidentsCount();
|
||||
res.json(counts);
|
||||
} catch (error) {
|
||||
logger.error('Error getting incidents count', error);
|
||||
res.status(500).json({ error: 'Failed to get incidents count' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/incidents/:id
|
||||
* Get incident by ID
|
||||
*/
|
||||
router.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const incident = await incidentsService.getIncident(id);
|
||||
|
||||
if (!incident) {
|
||||
return res.status(404).json({ error: 'Incident not found' });
|
||||
}
|
||||
|
||||
res.json(incident);
|
||||
} catch (error) {
|
||||
logger.error('Error getting incident', error);
|
||||
res.status(500).json({ error: 'Failed to get incident' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/incidents/:id/history
|
||||
* Get incident history
|
||||
*/
|
||||
router.get('/:id/history', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const history = await incidentsService.getIncidentHistory(id);
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
logger.error('Error getting incident history', error);
|
||||
res.status(500).json({ error: 'Failed to get incident history' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/incidents/:id/assign
|
||||
* Assign incident to an agent
|
||||
*/
|
||||
router.post('/:id/assign', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { assignedToDaisId, actorDaisId } = req.body;
|
||||
|
||||
if (!assignedToDaisId || !actorDaisId) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing required fields: assignedToDaisId, actorDaisId',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await incidentsService.assignIncident({
|
||||
incidentId: id,
|
||||
assignedToDaisId,
|
||||
actorDaisId,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Incident assigned' });
|
||||
} catch (error) {
|
||||
logger.error('Error assigning incident', error);
|
||||
res.status(500).json({ error: 'Failed to assign incident' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/incidents/:id/escalate
|
||||
* Escalate incident to higher level
|
||||
*/
|
||||
router.post('/:id/escalate', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { newLevel, actorDaisId, reason } = req.body;
|
||||
|
||||
if (!newLevel || !actorDaisId) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing required fields: newLevel, actorDaisId',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await incidentsService.escalateIncident({
|
||||
incidentId: id,
|
||||
newLevel: newLevel as EscalationLevel,
|
||||
actorDaisId,
|
||||
reason,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: `Incident escalated to ${newLevel}` });
|
||||
} catch (error) {
|
||||
logger.error('Error escalating incident', error);
|
||||
res.status(500).json({ error: 'Failed to escalate incident' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/incidents/:id/resolve
|
||||
* Resolve incident
|
||||
*/
|
||||
router.post('/:id/resolve', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { resolution, actorDaisId } = req.body;
|
||||
|
||||
if (!resolution || !actorDaisId) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing required fields: resolution, actorDaisId',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await incidentsService.resolveIncident({
|
||||
incidentId: id,
|
||||
resolution,
|
||||
actorDaisId,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Incident resolved' });
|
||||
} catch (error) {
|
||||
logger.error('Error resolving incident', error);
|
||||
res.status(500).json({ error: 'Failed to resolve incident' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/incidents/:id/close
|
||||
* Close incident
|
||||
*/
|
||||
router.post('/:id/close', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { actorDaisId } = req.body;
|
||||
|
||||
if (!actorDaisId) {
|
||||
return res.status(400).json({ error: 'Missing required field: actorDaisId' });
|
||||
}
|
||||
|
||||
const result = await incidentsService.closeIncident(id, actorDaisId);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Incident closed' });
|
||||
} catch (error) {
|
||||
logger.error('Error closing incident', error);
|
||||
res.status(500).json({ error: 'Failed to close incident' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/incidents/:id/comment
|
||||
* Add comment to incident
|
||||
*/
|
||||
router.post('/:id/comment', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { actorDaisId, comment } = req.body;
|
||||
|
||||
if (!actorDaisId || !comment) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing required fields: actorDaisId, comment',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await incidentsService.addComment(id, actorDaisId, comment);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Comment added' });
|
||||
} catch (error) {
|
||||
logger.error('Error adding comment to incident', error);
|
||||
res.status(500).json({ error: 'Failed to add comment' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user