@tantainnovative/create-ndpr 0.1.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -56
- package/bin/index.mjs +369 -394
- package/package.json +14 -4
- package/templates/drizzle-client.ts +44 -0
- package/templates/drizzle-schema.ts +224 -117
- package/templates/env-example +6 -3
- package/templates/express-breach-route.ts +98 -0
- package/templates/express-consent-route.ts +336 -79
- package/templates/express-cross-border-route.ts +152 -0
- package/templates/express-dpia-route.ts +332 -0
- package/templates/express-dsr-route.ts +76 -0
- package/templates/express-lawful-basis-route.ts +146 -0
- package/templates/express-request-context.ts +130 -0
- package/templates/github-ndpr-audit.yml +31 -0
- package/templates/ndpr-audit.json +14 -0
- package/templates/nextjs-breach-route.ts +145 -79
- package/templates/nextjs-consent-route.ts +343 -70
- package/templates/nextjs-cross-border-route.ts +314 -0
- package/templates/nextjs-dpia-route.ts +510 -0
- package/templates/nextjs-dsr-route.ts +93 -58
- package/templates/nextjs-lawful-basis-route.ts +283 -0
- package/templates/nextjs-layout.tsx +122 -50
- package/templates/nextjs-request-context.ts +137 -0
- package/templates/prisma-schema.prisma +121 -51
- package/templates/express-setup.ts +0 -250
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
export interface NDPRVerifiedActor {
|
|
4
|
+
/** Stable identity and profile fields from a verified server session. */
|
|
5
|
+
id: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
email: string;
|
|
8
|
+
department?: string;
|
|
9
|
+
/** Stable data-subject identifier for this account, when applicable. */
|
|
10
|
+
subjectId?: string;
|
|
11
|
+
/** Map application roles to ndpr:staff or ndpr:admin server-side. */
|
|
12
|
+
roles: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NDPRRequestContext {
|
|
16
|
+
/** Verified tenant boundary. Never take this value from request input. */
|
|
17
|
+
tenantId: string;
|
|
18
|
+
/** Normalized verified actor; null for public/anonymous requests. */
|
|
19
|
+
actor: NDPRVerifiedActor | null;
|
|
20
|
+
actorId: string | null;
|
|
21
|
+
/** Verified account subject ID or high-entropy anonymous browser ID. */
|
|
22
|
+
subjectId: string | null;
|
|
23
|
+
subjectSource: 'verified-account-subject' | 'anonymous-uuid-capability' | null;
|
|
24
|
+
/** Verified application roles used by your authorization policy. */
|
|
25
|
+
roles: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type NDPRContextRequirement = 'tenant' | 'subject' | 'staff';
|
|
29
|
+
export interface NDPRContextProblem { status: 401 | 403 | 503; error: string }
|
|
30
|
+
export type NDPRVerifiedActorResolver = (
|
|
31
|
+
request: NextRequest,
|
|
32
|
+
) => Promise<NDPRVerifiedActor | null>;
|
|
33
|
+
|
|
34
|
+
const ANONYMOUS_SUBJECT_PATTERN =
|
|
35
|
+
/^anon_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
36
|
+
const STAFF_ROLES = new Set(['ndpr:staff', 'ndpr:admin']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Authentication integration seam. Replace this safe default with your
|
|
40
|
+
* verified server session provider. Never trust actor, account subject,
|
|
41
|
+
* profile, role, or tenant fields from request-controlled input.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveVerifiedNDPRActor(
|
|
44
|
+
_request: NextRequest,
|
|
45
|
+
): Promise<NDPRVerifiedActor | null> {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The tenant comes only from server configuration. The only client-provided
|
|
51
|
+
* identity accepted by default is a random anon_<UUID> subject capability;
|
|
52
|
+
* it never grants staff access.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveNDPRRequestContext(
|
|
55
|
+
request: NextRequest,
|
|
56
|
+
actorResolver: NDPRVerifiedActorResolver = resolveVerifiedNDPRActor,
|
|
57
|
+
): Promise<NDPRRequestContext> {
|
|
58
|
+
const tenantId = process.env.NDPR_TENANT_ID?.trim() ?? '';
|
|
59
|
+
const candidate = request.headers.get('x-ndpr-subject-id')?.trim() ?? '';
|
|
60
|
+
const anonymousSubjectId = ANONYMOUS_SUBJECT_PATTERN.test(candidate)
|
|
61
|
+
? candidate
|
|
62
|
+
: null;
|
|
63
|
+
const actor = normalizeVerifiedActor(await actorResolver(request));
|
|
64
|
+
const subjectId = actor?.subjectId ?? anonymousSubjectId;
|
|
65
|
+
const subjectSource = actor?.subjectId
|
|
66
|
+
? 'verified-account-subject' as const
|
|
67
|
+
: anonymousSubjectId
|
|
68
|
+
? 'anonymous-uuid-capability' as const
|
|
69
|
+
: null;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
tenantId,
|
|
73
|
+
actor,
|
|
74
|
+
actorId: actor?.id ?? null,
|
|
75
|
+
subjectId,
|
|
76
|
+
subjectSource,
|
|
77
|
+
roles: actor?.roles ?? [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getNDPRContextProblem(
|
|
82
|
+
context: NDPRRequestContext,
|
|
83
|
+
requirement: NDPRContextRequirement,
|
|
84
|
+
): NDPRContextProblem | null {
|
|
85
|
+
if (!context.tenantId) {
|
|
86
|
+
return {
|
|
87
|
+
status: 503,
|
|
88
|
+
error: 'NDPR_TENANT_ID is not configured on the server',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (requirement === 'subject' && !context.subjectId) {
|
|
92
|
+
return { status: 401, error: 'A verified data-subject identity is required' };
|
|
93
|
+
}
|
|
94
|
+
if (requirement === 'staff' && !context.actorId) {
|
|
95
|
+
return {
|
|
96
|
+
status: 401,
|
|
97
|
+
error: 'Connect resolveVerifiedNDPRActor to verified staff authentication',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (requirement === 'staff'
|
|
101
|
+
&& !context.roles.some((role) => STAFF_ROLES.has(role))) {
|
|
102
|
+
return { status: 403, error: 'NDPR staff or administrator role required' };
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeVerifiedActor(
|
|
108
|
+
actor: NDPRVerifiedActor | null,
|
|
109
|
+
): NDPRVerifiedActor | null {
|
|
110
|
+
if (!actor
|
|
111
|
+
|| typeof actor.id !== 'string'
|
|
112
|
+
|| typeof actor.displayName !== 'string'
|
|
113
|
+
|| typeof actor.email !== 'string'
|
|
114
|
+
|| !Array.isArray(actor.roles)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const id = actor.id.trim();
|
|
118
|
+
const displayName = actor.displayName.trim();
|
|
119
|
+
const email = actor.email.trim();
|
|
120
|
+
if (!id || !displayName || !email) return null;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
displayName,
|
|
125
|
+
email,
|
|
126
|
+
department: typeof actor.department === 'string'
|
|
127
|
+
? actor.department.trim() || undefined
|
|
128
|
+
: undefined,
|
|
129
|
+
subjectId: typeof actor.subjectId === 'string'
|
|
130
|
+
? actor.subjectId.trim() || undefined
|
|
131
|
+
: undefined,
|
|
132
|
+
roles: actor.roles
|
|
133
|
+
.filter((role): role is string => typeof role === 'string')
|
|
134
|
+
.map((role) => role.trim())
|
|
135
|
+
.filter(Boolean),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
// NDPA
|
|
2
|
-
//
|
|
3
|
-
// DPO: {{DPO_EMAIL}}
|
|
4
|
-
//
|
|
5
|
-
// Covers:
|
|
6
|
-
// - ConsentRecord — NDPA §25–26 (consent & withdrawal)
|
|
7
|
-
// - DSRRequest — NDPA Part IV §34–38 (data subject rights)
|
|
8
|
-
// - BreachReport — NDPA §40 (72-hour breach notification)
|
|
9
|
-
// - ProcessingRecord — ROPA (accountability principle)
|
|
10
|
-
// - ComplianceAuditLog — §44 audit trail
|
|
11
|
-
//
|
|
12
|
-
// Run `prisma migrate dev --name ndpr-init` to apply.
|
|
1
|
+
// NDPA compliance schema — generated by create-ndpr for {{ORG_NAME_COMMENT}}
|
|
2
|
+
// Every row is tenant-scoped. Derive tenantId only from verified server context.
|
|
13
3
|
|
|
14
4
|
generator client {
|
|
15
5
|
provider = "prisma-client-js"
|
|
@@ -21,25 +11,32 @@ datasource db {
|
|
|
21
11
|
}
|
|
22
12
|
|
|
23
13
|
model ConsentRecord {
|
|
24
|
-
id
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
14
|
+
id String @id @default(cuid())
|
|
15
|
+
tenantId String
|
|
16
|
+
subjectId String
|
|
17
|
+
activeSubjectKey String? @unique
|
|
18
|
+
consents Json
|
|
19
|
+
version String
|
|
20
|
+
method String
|
|
21
|
+
hasInteracted Boolean
|
|
22
|
+
lawfulBasis String?
|
|
23
|
+
ipAddress String?
|
|
24
|
+
userAgent String?
|
|
25
|
+
clientTimestamp DateTime
|
|
26
|
+
createdAt DateTime @default(now())
|
|
27
|
+
revokedAt DateTime?
|
|
28
|
+
|
|
29
|
+
@@index([tenantId, subjectId, revokedAt])
|
|
30
|
+
@@index([tenantId, subjectId, clientTimestamp, version, method])
|
|
36
31
|
@@map("ndpr_consent_records")
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
model DSRRequest {
|
|
40
|
-
id String
|
|
35
|
+
id String @id @default(cuid())
|
|
36
|
+
tenantId String
|
|
37
|
+
subjectId String
|
|
41
38
|
type String
|
|
42
|
-
status String
|
|
39
|
+
status String @default("pending")
|
|
43
40
|
subjectName String
|
|
44
41
|
subjectEmail String
|
|
45
42
|
subjectPhone String?
|
|
@@ -48,43 +45,53 @@ model DSRRequest {
|
|
|
48
45
|
description String?
|
|
49
46
|
internalNotes String?
|
|
50
47
|
assignedTo String?
|
|
51
|
-
submittedAt DateTime
|
|
48
|
+
submittedAt DateTime @default(now())
|
|
52
49
|
acknowledgedAt DateTime?
|
|
53
50
|
completedAt DateTime?
|
|
54
51
|
dueAt DateTime
|
|
55
52
|
|
|
56
|
-
@@index([status])
|
|
57
|
-
@@index([
|
|
53
|
+
@@index([tenantId, status])
|
|
54
|
+
@@index([tenantId, subjectId])
|
|
55
|
+
@@index([tenantId, subjectEmail])
|
|
58
56
|
@@map("ndpr_dsr_requests")
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
model BreachReport {
|
|
62
|
-
id
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
60
|
+
id String @id @default(cuid())
|
|
61
|
+
tenantId String
|
|
62
|
+
title String
|
|
63
|
+
description String
|
|
64
|
+
category String
|
|
65
|
+
severity String
|
|
66
|
+
status String @default("ongoing")
|
|
67
|
+
discoveredAt DateTime
|
|
68
|
+
occurredAt DateTime?
|
|
69
|
+
reportedAt DateTime @default(now())
|
|
70
|
+
ndpcNotifiedAt DateTime?
|
|
71
|
+
reporterName String
|
|
72
|
+
reporterEmail String
|
|
73
|
+
reporterDepartment String?
|
|
74
|
+
affectedSystems Json
|
|
75
|
+
dataTypes Json
|
|
76
|
+
involvesSensitiveData Boolean @default(false)
|
|
77
|
+
estimatedAffected Int?
|
|
78
|
+
approximateRecordCount Int?
|
|
79
|
+
dataSubjectCategories Json
|
|
80
|
+
likelyConsequences String?
|
|
81
|
+
mitigationMeasures String?
|
|
82
|
+
isPhasedReport Boolean @default(false)
|
|
83
|
+
supplementsReportId String?
|
|
84
|
+
initialActions String?
|
|
85
|
+
ndpcNotificationSent Boolean @default(false)
|
|
86
|
+
|
|
87
|
+
@@index([tenantId, status])
|
|
88
|
+
@@index([tenantId, severity])
|
|
83
89
|
@@map("ndpr_breach_reports")
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
model ProcessingRecord {
|
|
87
93
|
id String @id @default(cuid())
|
|
94
|
+
tenantId String
|
|
88
95
|
purpose String
|
|
89
96
|
lawfulBasis String
|
|
90
97
|
dataCategories Json
|
|
@@ -99,11 +106,74 @@ model ProcessingRecord {
|
|
|
99
106
|
createdAt DateTime @default(now())
|
|
100
107
|
updatedAt DateTime @updatedAt
|
|
101
108
|
|
|
109
|
+
@@index([tenantId, status])
|
|
102
110
|
@@map("ndpr_processing_records")
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
model DPIARecord {
|
|
114
|
+
id String @id @default(cuid())
|
|
115
|
+
tenantId String
|
|
116
|
+
projectName String
|
|
117
|
+
description String
|
|
118
|
+
dpiaData Json
|
|
119
|
+
overallRisk String
|
|
120
|
+
score Int
|
|
121
|
+
status String @default("draft")
|
|
122
|
+
conductedBy String
|
|
123
|
+
approvedBy String?
|
|
124
|
+
createdAt DateTime @default(now())
|
|
125
|
+
updatedAt DateTime @updatedAt
|
|
126
|
+
removedAt DateTime?
|
|
127
|
+
|
|
128
|
+
@@index([tenantId, removedAt, status])
|
|
129
|
+
@@map("ndpr_dpia_records")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
model LawfulBasisRecord {
|
|
133
|
+
id String @id @default(cuid())
|
|
134
|
+
tenantId String
|
|
135
|
+
activityName String
|
|
136
|
+
lawfulBasis String
|
|
137
|
+
justification String
|
|
138
|
+
dataCategories Json
|
|
139
|
+
purposes Json
|
|
140
|
+
assessedBy String
|
|
141
|
+
assessedAt DateTime @default(now())
|
|
142
|
+
reviewDate DateTime?
|
|
143
|
+
createdAt DateTime @default(now())
|
|
144
|
+
updatedAt DateTime @updatedAt
|
|
145
|
+
removedAt DateTime?
|
|
146
|
+
|
|
147
|
+
@@index([tenantId, removedAt, lawfulBasis])
|
|
148
|
+
@@map("ndpr_lawful_basis_records")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
model CrossBorderTransferRecord {
|
|
152
|
+
id String @id @default(cuid())
|
|
153
|
+
tenantId String
|
|
154
|
+
destinationCountry String
|
|
155
|
+
recipientName String
|
|
156
|
+
transferMechanism String
|
|
157
|
+
safeguards String
|
|
158
|
+
dataCategories Json
|
|
159
|
+
adequacyStatus String
|
|
160
|
+
ndpcApprovalRequired Boolean @default(false)
|
|
161
|
+
ndpcApprovalReference String?
|
|
162
|
+
riskLevel String
|
|
163
|
+
status String @default("active")
|
|
164
|
+
createdAt DateTime @default(now())
|
|
165
|
+
updatedAt DateTime @updatedAt
|
|
166
|
+
removedAt DateTime?
|
|
167
|
+
|
|
168
|
+
@@index([tenantId, removedAt, destinationCountry])
|
|
169
|
+
@@index([tenantId, removedAt, riskLevel])
|
|
170
|
+
@@index([tenantId, removedAt, status])
|
|
171
|
+
@@map("ndpr_cross_border_transfer_records")
|
|
172
|
+
}
|
|
173
|
+
|
|
105
174
|
model ComplianceAuditLog {
|
|
106
175
|
id String @id @default(cuid())
|
|
176
|
+
tenantId String
|
|
107
177
|
module String
|
|
108
178
|
action String
|
|
109
179
|
entityId String
|
|
@@ -112,6 +182,6 @@ model ComplianceAuditLog {
|
|
|
112
182
|
performedBy String?
|
|
113
183
|
createdAt DateTime @default(now())
|
|
114
184
|
|
|
115
|
-
@@index([module, entityId])
|
|
185
|
+
@@index([tenantId, module, entityId])
|
|
116
186
|
@@map("ndpr_audit_log")
|
|
117
187
|
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Express — NDPR Compliance Router
|
|
3
|
-
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
|
-
* DPO: {{DPO_EMAIL}}
|
|
5
|
-
*
|
|
6
|
-
* Mount this router in your Express app:
|
|
7
|
-
*
|
|
8
|
-
* import express from 'express';
|
|
9
|
-
* import cookieParser from 'cookie-parser';
|
|
10
|
-
* import { createNDPRRouter } from './src/ndpr';
|
|
11
|
-
*
|
|
12
|
-
* const app = express();
|
|
13
|
-
* app.use(express.json());
|
|
14
|
-
* app.use(cookieParser());
|
|
15
|
-
* app.use('/api/ndpr', createNDPRRouter());
|
|
16
|
-
*
|
|
17
|
-
* Routes exposed:
|
|
18
|
-
* GET/POST/DELETE /api/ndpr/consent — NDPA §25–26
|
|
19
|
-
* GET/POST /api/ndpr/dsr — NDPA §34–38
|
|
20
|
-
* GET/POST /api/ndpr/breach — NDPA §40
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { Router } from 'express';
|
|
24
|
-
import { PrismaClient } from '@prisma/client';
|
|
25
|
-
|
|
26
|
-
const prisma = new PrismaClient();
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Consent router — NDPA §25–26
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
function consentRouter(): Router {
|
|
33
|
-
const router = Router();
|
|
34
|
-
|
|
35
|
-
// GET /consent?subjectId=xxx
|
|
36
|
-
router.get('/', async (req, res) => {
|
|
37
|
-
const { subjectId } = req.query;
|
|
38
|
-
if (!subjectId || typeof subjectId !== 'string') {
|
|
39
|
-
return res.status(400).json({ error: 'subjectId required' });
|
|
40
|
-
}
|
|
41
|
-
const record = await prisma.consentRecord.findFirst({
|
|
42
|
-
where: { subjectId, revokedAt: null },
|
|
43
|
-
orderBy: { createdAt: 'desc' },
|
|
44
|
-
});
|
|
45
|
-
return res.json(record);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// POST /consent
|
|
49
|
-
router.post('/', async (req, res) => {
|
|
50
|
-
const { subjectId, consents, version, method, lawfulBasis } = req.body;
|
|
51
|
-
if (!subjectId || !consents || !version) {
|
|
52
|
-
return res.status(400).json({ error: 'subjectId, consents, and version are required' });
|
|
53
|
-
}
|
|
54
|
-
await prisma.consentRecord.updateMany({
|
|
55
|
-
where: { subjectId, revokedAt: null },
|
|
56
|
-
data: { revokedAt: new Date() },
|
|
57
|
-
});
|
|
58
|
-
const record = await prisma.consentRecord.create({
|
|
59
|
-
data: {
|
|
60
|
-
subjectId,
|
|
61
|
-
consents,
|
|
62
|
-
version,
|
|
63
|
-
method: method ?? 'api',
|
|
64
|
-
lawfulBasis: lawfulBasis ?? null,
|
|
65
|
-
ipAddress:
|
|
66
|
-
(req.headers['x-forwarded-for'] as string)?.split(',')[0].trim() ??
|
|
67
|
-
req.socket.remoteAddress ??
|
|
68
|
-
null,
|
|
69
|
-
userAgent: req.headers['user-agent'] ?? null,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
await prisma.complianceAuditLog.create({
|
|
73
|
-
data: {
|
|
74
|
-
module: 'consent',
|
|
75
|
-
action: 'created',
|
|
76
|
-
entityId: record.id,
|
|
77
|
-
entityType: 'ConsentRecord',
|
|
78
|
-
changes: { subjectId, version, consents },
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
return res.status(201).json(record);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// DELETE /consent?subjectId=xxx
|
|
85
|
-
router.delete('/', async (req, res) => {
|
|
86
|
-
const { subjectId } = req.query;
|
|
87
|
-
if (!subjectId || typeof subjectId !== 'string') {
|
|
88
|
-
return res.status(400).json({ error: 'subjectId required' });
|
|
89
|
-
}
|
|
90
|
-
await prisma.consentRecord.updateMany({
|
|
91
|
-
where: { subjectId, revokedAt: null },
|
|
92
|
-
data: { revokedAt: new Date() },
|
|
93
|
-
});
|
|
94
|
-
await prisma.complianceAuditLog.create({
|
|
95
|
-
data: {
|
|
96
|
-
module: 'consent',
|
|
97
|
-
action: 'revoked',
|
|
98
|
-
entityId: subjectId,
|
|
99
|
-
entityType: 'ConsentRecord',
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
return res.json({ success: true });
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return router;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// DSR router — NDPA §34–38
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
function dsrRouter(): Router {
|
|
113
|
-
const router = Router();
|
|
114
|
-
|
|
115
|
-
// GET /dsr?status=pending
|
|
116
|
-
router.get('/', async (req, res) => {
|
|
117
|
-
const { status } = req.query;
|
|
118
|
-
const requests = await prisma.dSRRequest.findMany({
|
|
119
|
-
where: status && typeof status === 'string' ? { status } : undefined,
|
|
120
|
-
orderBy: { submittedAt: 'desc' },
|
|
121
|
-
});
|
|
122
|
-
return res.json(requests);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// POST /dsr
|
|
126
|
-
router.post('/', async (req, res) => {
|
|
127
|
-
const { type, subjectName, subjectEmail, subjectPhone, identifierType, identifierValue, description } =
|
|
128
|
-
req.body;
|
|
129
|
-
if (!type || !subjectName || !subjectEmail || !identifierType || !identifierValue) {
|
|
130
|
-
return res
|
|
131
|
-
.status(400)
|
|
132
|
-
.json({ error: 'type, subjectName, subjectEmail, identifierType, and identifierValue are required' });
|
|
133
|
-
}
|
|
134
|
-
const dueAt = new Date();
|
|
135
|
-
dueAt.setDate(dueAt.getDate() + 30);
|
|
136
|
-
const request = await prisma.dSRRequest.create({
|
|
137
|
-
data: {
|
|
138
|
-
type,
|
|
139
|
-
subjectName,
|
|
140
|
-
subjectEmail,
|
|
141
|
-
subjectPhone: subjectPhone ?? null,
|
|
142
|
-
identifierType,
|
|
143
|
-
identifierValue,
|
|
144
|
-
description: description ?? null,
|
|
145
|
-
status: 'pending',
|
|
146
|
-
dueAt,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
await prisma.complianceAuditLog.create({
|
|
150
|
-
data: {
|
|
151
|
-
module: 'dsr',
|
|
152
|
-
action: 'submitted',
|
|
153
|
-
entityId: request.id,
|
|
154
|
-
entityType: 'DSRRequest',
|
|
155
|
-
changes: { type, subjectEmail, status: 'pending' },
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
return res.status(201).json(request);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return router;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
// Breach router — NDPA §40
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
|
|
168
|
-
function breachRouter(): Router {
|
|
169
|
-
const router = Router();
|
|
170
|
-
|
|
171
|
-
function calculateSeverity(category: string, estimatedAffected?: number) {
|
|
172
|
-
const highRisk = ['unauthorized_access', 'ransomware', 'data_exfiltration', 'identity_theft'];
|
|
173
|
-
if (highRisk.includes(category)) return (estimatedAffected ?? 0) > 1000 ? 'critical' : 'high';
|
|
174
|
-
if ((estimatedAffected ?? 0) > 500) return 'high';
|
|
175
|
-
if ((estimatedAffected ?? 0) > 50) return 'medium';
|
|
176
|
-
return 'low';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// GET /breach?status=ongoing
|
|
180
|
-
router.get('/', async (req, res) => {
|
|
181
|
-
const { status } = req.query;
|
|
182
|
-
const reports = await prisma.breachReport.findMany({
|
|
183
|
-
where: status && typeof status === 'string' ? { status } : undefined,
|
|
184
|
-
orderBy: { reportedAt: 'desc' },
|
|
185
|
-
});
|
|
186
|
-
return res.json(reports);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// POST /breach
|
|
190
|
-
router.post('/', async (req, res) => {
|
|
191
|
-
const {
|
|
192
|
-
title, description, category, discoveredAt, occurredAt,
|
|
193
|
-
reporterName, reporterEmail, reporterDepartment,
|
|
194
|
-
affectedSystems, dataTypes, estimatedAffected, initialActions,
|
|
195
|
-
} = req.body;
|
|
196
|
-
|
|
197
|
-
if (!title || !description || !category || !discoveredAt || !reporterName || !reporterEmail) {
|
|
198
|
-
return res.status(400).json({
|
|
199
|
-
error: 'title, description, category, discoveredAt, reporterName, and reporterEmail are required',
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
if (!Array.isArray(affectedSystems) || !Array.isArray(dataTypes)) {
|
|
203
|
-
return res.status(400).json({ error: 'affectedSystems and dataTypes must be arrays' });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const severity = calculateSeverity(category, estimatedAffected);
|
|
207
|
-
const report = await prisma.breachReport.create({
|
|
208
|
-
data: {
|
|
209
|
-
title, description, category, severity, status: 'ongoing',
|
|
210
|
-
discoveredAt: new Date(discoveredAt),
|
|
211
|
-
occurredAt: occurredAt ? new Date(occurredAt) : null,
|
|
212
|
-
reporterName, reporterEmail,
|
|
213
|
-
reporterDepartment: reporterDepartment ?? null,
|
|
214
|
-
affectedSystems, dataTypes,
|
|
215
|
-
estimatedAffected: estimatedAffected ?? null,
|
|
216
|
-
initialActions: initialActions ?? null,
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
await prisma.complianceAuditLog.create({
|
|
220
|
-
data: {
|
|
221
|
-
module: 'breach',
|
|
222
|
-
action: 'reported',
|
|
223
|
-
entityId: report.id,
|
|
224
|
-
entityType: 'BreachReport',
|
|
225
|
-
changes: { title, category, severity, status: 'ongoing' },
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
return res.status(201).json(report);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
return router;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
// Main factory
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Returns a fully configured NDPR compliance Express Router.
|
|
240
|
-
*
|
|
241
|
-
* Mount with:
|
|
242
|
-
* app.use('/api/ndpr', createNDPRRouter());
|
|
243
|
-
*/
|
|
244
|
-
export function createNDPRRouter(): Router {
|
|
245
|
-
const router = Router();
|
|
246
|
-
router.use('/consent', consentRouter());
|
|
247
|
-
router.use('/dsr', dsrRouter());
|
|
248
|
-
router.use('/breach', breachRouter());
|
|
249
|
-
return router;
|
|
250
|
-
}
|