Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 60 additions & 73 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"@opentelemetry/sdk-trace-node": "^2.8.0",
"@opentelemetry/semantic-conventions": "^1.41.1",
"@prisma/client": "^7.8.0",
"@prisma/config": "^7.10.0",
"@types/react": "^19.2.17",
"axios": "^1.17.0",
"class-transformer": "^0.5.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
-- CreateEnum
CREATE TYPE "CaseStatus" AS ENUM ('DRAFT', 'ACTIVE', 'IN_REVIEW', 'SUSPENDED', 'CLOSED', 'ARCHIVED');

-- CreateEnum
CREATE TYPE "CaseSeverity" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL');

-- CreateEnum
CREATE TYPE "CaseAssigneeRole" AS ENUM ('INVESTIGATOR', 'LEGAL_OBSERVER', 'EXTERNAL_AUDITOR');

-- CreateTable
CREATE TABLE "cases" (
"id" TEXT NOT NULL,
"caseNumber" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"status" "CaseStatus" NOT NULL DEFAULT 'DRAFT',
"severity" "CaseSeverity" NOT NULL DEFAULT 'MEDIUM',
"leadInvestigatorId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"closedAt" TIMESTAMP(3),
"nextReviewAt" TIMESTAMP(3),

CONSTRAINT "cases_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "case_assignees" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"role" "CaseAssigneeRole" NOT NULL DEFAULT 'INVESTIGATOR',
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "case_assignees_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "case_incidents" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"incidentId" TEXT NOT NULL,
"linkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "case_incidents_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "case_activity_logs" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"actorId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"ipAddress" TEXT,
"changedFields" JSONB,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "case_activity_logs_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "cases_caseNumber_key" ON "cases"("caseNumber");

-- CreateIndex
CREATE INDEX "cases_status_idx" ON "cases"("status");

-- CreateIndex
CREATE INDEX "cases_severity_idx" ON "cases"("severity");

-- CreateIndex
CREATE INDEX "cases_leadInvestigatorId_idx" ON "cases"("leadInvestigatorId");

-- CreateIndex
CREATE INDEX "cases_caseNumber_idx" ON "cases"("caseNumber");

-- CreateIndex
CREATE UNIQUE INDEX "case_assignees_caseId_userId_key" ON "case_assignees"("caseId", "userId");

-- CreateIndex
CREATE INDEX "case_assignees_userId_idx" ON "case_assignees"("userId");

-- CreateIndex
CREATE UNIQUE INDEX "case_incidents_caseId_incidentId_key" ON "case_incidents"("caseId", "incidentId");

-- CreateIndex
CREATE INDEX "case_incidents_incidentId_idx" ON "case_incidents"("incidentId");

-- CreateIndex
CREATE INDEX "case_activity_logs_caseId_idx" ON "case_activity_logs"("caseId");

-- CreateIndex
CREATE INDEX "case_activity_logs_actorId_idx" ON "case_activity_logs"("actorId");

-- AddForeignKey
ALTER TABLE "cases" ADD CONSTRAINT "cases_leadInvestigatorId_fkey" FOREIGN KEY ("leadInvestigatorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "case_assignees" ADD CONSTRAINT "case_assignees_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "case_assignees" ADD CONSTRAINT "case_assignees_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "case_incidents" ADD CONSTRAINT "case_incidents_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "case_incidents" ADD CONSTRAINT "case_incidents_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "incidents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "case_activity_logs" ADD CONSTRAINT "case_activity_logs_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "case_activity_logs" ADD CONSTRAINT "case_activity_logs_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "investigation_notes" ADD CONSTRAINT "investigation_notes_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
103 changes: 103 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ model User {
memberships Membership[]
invitationsSent Invitation[]
ownedOrganizations Organization[] @relation("OrganizationOwner")
leadCases Case[] @relation("LeadInvestigator")
caseAssignments CaseAssignee[]
caseActivities CaseActivityLog[] @relation("CaseActivityActor")

@@map("users")
}
Expand Down Expand Up @@ -163,6 +166,7 @@ model InvestigationNote {
updatedAt DateTime @updatedAt

auditEntries NoteAuditLog[]
case Case? @relation(fields: [caseId], references: [id], onDelete: Cascade)

@@index([caseId])
@@map("investigation_notes")
Expand Down Expand Up @@ -211,6 +215,7 @@ model Incident {
assignedUser User? @relation(fields: [assignedUserId], references: [id])
alerts Alert[]
auditEntries IncidentAuditLog[]
caseIncidents CaseIncident[]

@@index([status])
@@index([severity])
Expand All @@ -236,6 +241,104 @@ model IncidentAuditLog {
@@map("incident_audit_logs")
}

// ---------------------------------------------------------------------------
// Forensic Investigation & Case Management – issue #126
// ---------------------------------------------------------------------------

enum CaseStatus {
DRAFT
ACTIVE
IN_REVIEW
SUSPENDED
CLOSED
ARCHIVED
}

enum CaseSeverity {
LOW
MEDIUM
HIGH
CRITICAL
}

enum CaseAssigneeRole {
INVESTIGATOR
LEGAL_OBSERVER
EXTERNAL_AUDITOR
}

model Case {
id String @id @default(uuid())
caseNumber String @unique
title String
description String
status CaseStatus @default(DRAFT)
severity CaseSeverity @default(MEDIUM)
leadInvestigatorId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
closedAt DateTime?
nextReviewAt DateTime?

leadInvestigator User? @relation("LeadInvestigator", fields: [leadInvestigatorId], references: [id], onDelete: SetNull)
assignees CaseAssignee[]
incidents CaseIncident[]
activityLogs CaseActivityLog[]
notes InvestigationNote[]

@@index([status])
@@index([severity])
@@index([leadInvestigatorId])
@@index([caseNumber])
@@map("cases")
}

model CaseAssignee {
id String @id @default(uuid())
caseId String
userId String
role CaseAssigneeRole @default(INVESTIGATOR)
assignedAt DateTime @default(now())

case Case @relation(fields: [caseId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Restrict)

@@unique([caseId, userId])
@@index([userId])
@@map("case_assignees")
}

model CaseIncident {
id String @id @default(uuid())
caseId String
incidentId String
linkedAt DateTime @default(now())

case Case @relation(fields: [caseId], references: [id], onDelete: Restrict)
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Restrict)

@@unique([caseId, incidentId])
@@index([incidentId])
@@map("case_incidents")
}

model CaseActivityLog {
id String @id @default(uuid())
caseId String
actorId String
action String
ipAddress String?
changedFields Json?
timestamp DateTime @default(now())

case Case @relation(fields: [caseId], references: [id], onDelete: Restrict)
actor User @relation("CaseActivityActor", fields: [actorId], references: [id], onDelete: Restrict)

@@index([caseId])
@@index([actorId])
@@map("case_activity_logs")
}

// ---------------------------------------------------------------------------
// Protocol Health Monitoring
// ---------------------------------------------------------------------------
Expand Down
132 changes: 132 additions & 0 deletions src/domain/cases/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Forensic Investigation & Case Management Domain Types (#126)
*/

export enum CaseStatus {
DRAFT = 'DRAFT',
ACTIVE = 'ACTIVE',
IN_REVIEW = 'IN_REVIEW',
SUSPENDED = 'SUSPENDED',
CLOSED = 'CLOSED',
ARCHIVED = 'ARCHIVED',
}

export enum CaseSeverity {
LOW = 'LOW',
MEDIUM = 'MEDIUM',
HIGH = 'HIGH',
CRITICAL = 'CRITICAL',
}

export enum CaseAssigneeRole {
INVESTIGATOR = 'INVESTIGATOR',
LEGAL_OBSERVER = 'LEGAL_OBSERVER',
EXTERNAL_AUDITOR = 'EXTERNAL_AUDITOR',
}

export interface CaseUserSummary {
id: string;
email: string;
}

export interface CaseIncidentSummary {
id: string;
title: string;
severity: string;
status: string;
}

export interface CaseAssignee {
id: string;
caseId: string;
userId: string;
role: CaseAssigneeRole;
assignedAt: Date;
user?: CaseUserSummary;
}

export interface CaseIncident {
id: string;
caseId: string;
incidentId: string;
linkedAt: Date;
incident?: CaseIncidentSummary;
}

export interface CaseActivityLog {
id: string;
caseId: string;
actorId: string;
action: string;
ipAddress?: string | null;
changedFields?: Record<string, { from: unknown; to: unknown }> | null;
timestamp: Date;
actor?: CaseUserSummary;
}

export interface Case {
id: string;
caseNumber: string;
title: string;
description: string;
status: CaseStatus;
severity: CaseSeverity;
leadInvestigatorId?: string | null;
createdAt: Date;
updatedAt: Date;
closedAt?: Date | null;
nextReviewAt?: Date | null;
leadInvestigator?: CaseUserSummary | null;
assignees?: CaseAssignee[];
incidents?: CaseIncident[];
activityLogs?: CaseActivityLog[];
}

export interface CreateCaseInput {
caseNumber?: string;
title: string;
description: string;
status?: CaseStatus;
severity?: CaseSeverity;
leadInvestigatorId?: string | null;
nextReviewAt?: Date | null;
assigneeUserIds?: Array<{ userId: string; role?: CaseAssigneeRole }>;
incidentIds?: string[];
actorId: string;
ipAddress?: string;
}

export interface UpdateCaseInput {
title?: string;
description?: string;
status?: CaseStatus;
severity?: CaseSeverity;
leadInvestigatorId?: string | null;
nextReviewAt?: Date | null;
closedAt?: Date | null;
actorId: string;
ipAddress?: string;
}

export interface AddCaseAssigneeInput {
caseId: string;
userId: string;
role: CaseAssigneeRole;
actorId: string;
ipAddress?: string;
}

export interface LinkCaseIncidentInput {
caseId: string;
incidentId: string;
actorId: string;
ipAddress?: string;
}

export interface LogCaseActivityInput {
caseId: string;
actorId: string;
action: string;
ipAddress?: string | null;
changedFields?: Record<string, { from: unknown; to: unknown }> | null;
}
Loading
Loading