@tantainnovative/create-ndpr 0.4.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.
@@ -1,509 +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
- * CRUD /api/ndpr/dpia — NDPA §34(3)
22
- * CRUD /api/ndpr/lawful-basis — NDPA §25
23
- * CRUD /api/ndpr/cross-border — NDPA §41–42
24
- */
25
-
26
- import { Router } from 'express';
27
- import { PrismaClient } from '@prisma/client';
28
-
29
- const prisma = new PrismaClient();
30
-
31
- // ---------------------------------------------------------------------------
32
- // Consent router — NDPA §25–26
33
- // ---------------------------------------------------------------------------
34
-
35
- function consentRouter(): Router {
36
- const router = Router();
37
-
38
- // GET /consent?subjectId=xxx
39
- router.get('/', async (req, res) => {
40
- const { subjectId } = req.query;
41
- if (!subjectId || typeof subjectId !== 'string') {
42
- return res.status(400).json({ error: 'subjectId required' });
43
- }
44
- const record = await prisma.consentRecord.findFirst({
45
- where: { subjectId, revokedAt: null },
46
- orderBy: { createdAt: 'desc' },
47
- });
48
- return res.json(record);
49
- });
50
-
51
- // POST /consent
52
- router.post('/', async (req, res) => {
53
- const { subjectId, consents, version, method, lawfulBasis } = req.body;
54
- if (!subjectId || !consents || !version) {
55
- return res.status(400).json({ error: 'subjectId, consents, and version are required' });
56
- }
57
- await prisma.consentRecord.updateMany({
58
- where: { subjectId, revokedAt: null },
59
- data: { revokedAt: new Date() },
60
- });
61
- const record = await prisma.consentRecord.create({
62
- data: {
63
- subjectId,
64
- consents,
65
- version,
66
- method: method ?? 'api',
67
- lawfulBasis: lawfulBasis ?? null,
68
- ipAddress:
69
- (req.headers['x-forwarded-for'] as string)?.split(',')[0].trim() ??
70
- req.socket.remoteAddress ??
71
- null,
72
- userAgent: req.headers['user-agent'] ?? null,
73
- },
74
- });
75
- await prisma.complianceAuditLog.create({
76
- data: {
77
- module: 'consent',
78
- action: 'created',
79
- entityId: record.id,
80
- entityType: 'ConsentRecord',
81
- changes: { subjectId, version, consents },
82
- },
83
- });
84
- return res.status(201).json(record);
85
- });
86
-
87
- // DELETE /consent?subjectId=xxx
88
- router.delete('/', async (req, res) => {
89
- const { subjectId } = req.query;
90
- if (!subjectId || typeof subjectId !== 'string') {
91
- return res.status(400).json({ error: 'subjectId required' });
92
- }
93
- await prisma.consentRecord.updateMany({
94
- where: { subjectId, revokedAt: null },
95
- data: { revokedAt: new Date() },
96
- });
97
- await prisma.complianceAuditLog.create({
98
- data: {
99
- module: 'consent',
100
- action: 'revoked',
101
- entityId: subjectId,
102
- entityType: 'ConsentRecord',
103
- },
104
- });
105
- return res.json({ success: true });
106
- });
107
-
108
- return router;
109
- }
110
-
111
- // ---------------------------------------------------------------------------
112
- // DSR router — NDPA §34–38
113
- // ---------------------------------------------------------------------------
114
-
115
- function dsrRouter(): Router {
116
- const router = Router();
117
-
118
- // GET /dsr?status=pending
119
- router.get('/', async (req, res) => {
120
- const { status } = req.query;
121
- const requests = await prisma.dSRRequest.findMany({
122
- where: status && typeof status === 'string' ? { status } : undefined,
123
- orderBy: { submittedAt: 'desc' },
124
- });
125
- return res.json(requests);
126
- });
127
-
128
- // POST /dsr
129
- router.post('/', async (req, res) => {
130
- const { type, subjectName, subjectEmail, subjectPhone, identifierType, identifierValue, description } =
131
- req.body;
132
- if (!type || !subjectName || !subjectEmail || !identifierType || !identifierValue) {
133
- return res
134
- .status(400)
135
- .json({ error: 'type, subjectName, subjectEmail, identifierType, and identifierValue are required' });
136
- }
137
- const dueAt = new Date();
138
- dueAt.setDate(dueAt.getDate() + 30);
139
- const request = await prisma.dSRRequest.create({
140
- data: {
141
- type,
142
- subjectName,
143
- subjectEmail,
144
- subjectPhone: subjectPhone ?? null,
145
- identifierType,
146
- identifierValue,
147
- description: description ?? null,
148
- status: 'pending',
149
- dueAt,
150
- },
151
- });
152
- await prisma.complianceAuditLog.create({
153
- data: {
154
- module: 'dsr',
155
- action: 'submitted',
156
- entityId: request.id,
157
- entityType: 'DSRRequest',
158
- changes: { type, subjectEmail, status: 'pending' },
159
- },
160
- });
161
- return res.status(201).json(request);
162
- });
163
-
164
- return router;
165
- }
166
-
167
- // ---------------------------------------------------------------------------
168
- // Breach router — NDPA §40
169
- // ---------------------------------------------------------------------------
170
-
171
- function breachRouter(): Router {
172
- const router = Router();
173
-
174
- function calculateSeverity(category: string, estimatedAffected?: number) {
175
- const highRisk = ['unauthorized_access', 'ransomware', 'data_exfiltration', 'identity_theft'];
176
- if (highRisk.includes(category)) return (estimatedAffected ?? 0) > 1000 ? 'critical' : 'high';
177
- if ((estimatedAffected ?? 0) > 500) return 'high';
178
- if ((estimatedAffected ?? 0) > 50) return 'medium';
179
- return 'low';
180
- }
181
-
182
- // GET /breach?status=ongoing
183
- router.get('/', async (req, res) => {
184
- const { status } = req.query;
185
- const reports = await prisma.breachReport.findMany({
186
- where: status && typeof status === 'string' ? { status } : undefined,
187
- orderBy: { reportedAt: 'desc' },
188
- });
189
- return res.json(reports);
190
- });
191
-
192
- // POST /breach
193
- router.post('/', async (req, res) => {
194
- const {
195
- title, description, category, discoveredAt, occurredAt,
196
- reporterName, reporterEmail, reporterDepartment,
197
- affectedSystems, dataTypes, estimatedAffected, initialActions,
198
- } = req.body;
199
-
200
- if (!title || !description || !category || !discoveredAt || !reporterName || !reporterEmail) {
201
- return res.status(400).json({
202
- error: 'title, description, category, discoveredAt, reporterName, and reporterEmail are required',
203
- });
204
- }
205
- if (!Array.isArray(affectedSystems) || !Array.isArray(dataTypes)) {
206
- return res.status(400).json({ error: 'affectedSystems and dataTypes must be arrays' });
207
- }
208
-
209
- const severity = calculateSeverity(category, estimatedAffected);
210
- const report = await prisma.breachReport.create({
211
- data: {
212
- title, description, category, severity, status: 'ongoing',
213
- discoveredAt: new Date(discoveredAt),
214
- occurredAt: occurredAt ? new Date(occurredAt) : null,
215
- reporterName, reporterEmail,
216
- reporterDepartment: reporterDepartment ?? null,
217
- affectedSystems, dataTypes,
218
- estimatedAffected: estimatedAffected ?? null,
219
- initialActions: initialActions ?? null,
220
- },
221
- });
222
- await prisma.complianceAuditLog.create({
223
- data: {
224
- module: 'breach',
225
- action: 'reported',
226
- entityId: report.id,
227
- entityType: 'BreachReport',
228
- changes: { title, category, severity, status: 'ongoing' },
229
- },
230
- });
231
- return res.status(201).json(report);
232
- });
233
-
234
- return router;
235
- }
236
-
237
- // ---------------------------------------------------------------------------
238
- // DPIA router — NDPA §34(3)
239
- // ---------------------------------------------------------------------------
240
-
241
- function dpiaRouter(): Router {
242
- const router = Router();
243
-
244
- // GET /dpia?status=draft
245
- router.get('/', async (req, res) => {
246
- const { status } = req.query;
247
- const records = await prisma.dPIARecord.findMany({
248
- where: status && typeof status === 'string' ? { status } : undefined,
249
- orderBy: { createdAt: 'desc' },
250
- });
251
- return res.json(records);
252
- });
253
-
254
- // GET /dpia/:id
255
- router.get('/:id', async (req, res) => {
256
- const record = await prisma.dPIARecord.findUnique({ where: { id: req.params.id } });
257
- if (!record) return res.status(404).json({ error: 'DPIA record not found' });
258
- return res.json(record);
259
- });
260
-
261
- // POST /dpia
262
- router.post('/', async (req, res) => {
263
- const { projectName, description, dpiaData, overallRisk, score, conductedBy, approvedBy } = req.body;
264
- if (!projectName || !description || !dpiaData || !overallRisk || score == null || !conductedBy) {
265
- return res.status(400).json({
266
- error: 'projectName, description, dpiaData, overallRisk, score, and conductedBy are required',
267
- });
268
- }
269
- const record = await prisma.dPIARecord.create({
270
- data: {
271
- projectName, description, dpiaData, overallRisk, score,
272
- status: 'draft', conductedBy, approvedBy: approvedBy ?? null,
273
- },
274
- });
275
- await prisma.complianceAuditLog.create({
276
- data: {
277
- module: 'dpia', action: 'created', entityId: record.id,
278
- entityType: 'DPIARecord', changes: { projectName, overallRisk, score, status: 'draft' },
279
- },
280
- });
281
- return res.status(201).json(record);
282
- });
283
-
284
- // PUT /dpia/:id
285
- router.put('/:id', async (req, res) => {
286
- const { id } = req.params;
287
- const existing = await prisma.dPIARecord.findUnique({ where: { id } });
288
- if (!existing) return res.status(404).json({ error: 'DPIA record not found' });
289
- const record = await prisma.dPIARecord.update({ where: { id }, data: req.body });
290
- await prisma.complianceAuditLog.create({
291
- data: { module: 'dpia', action: 'updated', entityId: record.id, entityType: 'DPIARecord', changes: req.body },
292
- });
293
- return res.json(record);
294
- });
295
-
296
- // DELETE /dpia/:id
297
- router.delete('/:id', async (req, res) => {
298
- const { id } = req.params;
299
- const existing = await prisma.dPIARecord.findUnique({ where: { id } });
300
- if (!existing) return res.status(404).json({ error: 'DPIA record not found' });
301
- await prisma.dPIARecord.delete({ where: { id } });
302
- await prisma.complianceAuditLog.create({
303
- data: {
304
- module: 'dpia', action: 'deleted', entityId: id,
305
- entityType: 'DPIARecord', changes: { projectName: existing.projectName },
306
- },
307
- });
308
- return res.json({ success: true });
309
- });
310
-
311
- return router;
312
- }
313
-
314
- // ---------------------------------------------------------------------------
315
- // Lawful Basis router — NDPA §25
316
- // ---------------------------------------------------------------------------
317
-
318
- function lawfulBasisRouter(): Router {
319
- const router = Router();
320
-
321
- // GET /lawful-basis?lawfulBasis=consent
322
- router.get('/', async (req, res) => {
323
- const { lawfulBasis } = req.query;
324
- const records = await prisma.lawfulBasisRecord.findMany({
325
- where: lawfulBasis && typeof lawfulBasis === 'string' ? { lawfulBasis } : undefined,
326
- orderBy: { createdAt: 'desc' },
327
- });
328
- return res.json(records);
329
- });
330
-
331
- // GET /lawful-basis/:id
332
- router.get('/:id', async (req, res) => {
333
- const record = await prisma.lawfulBasisRecord.findUnique({ where: { id: req.params.id } });
334
- if (!record) return res.status(404).json({ error: 'Lawful basis record not found' });
335
- return res.json(record);
336
- });
337
-
338
- // POST /lawful-basis
339
- router.post('/', async (req, res) => {
340
- const { activityName, lawfulBasis, justification, dataCategories, purposes, assessedBy, reviewDate } = req.body;
341
- if (!activityName || !lawfulBasis || !justification || !dataCategories || !purposes || !assessedBy) {
342
- return res.status(400).json({
343
- error: 'activityName, lawfulBasis, justification, dataCategories, purposes, and assessedBy are required',
344
- });
345
- }
346
- if (!Array.isArray(dataCategories) || !Array.isArray(purposes)) {
347
- return res.status(400).json({ error: 'dataCategories and purposes must be arrays' });
348
- }
349
- const record = await prisma.lawfulBasisRecord.create({
350
- data: {
351
- activityName, lawfulBasis, justification, dataCategories, purposes,
352
- assessedBy, reviewDate: reviewDate ? new Date(reviewDate) : null,
353
- },
354
- });
355
- await prisma.complianceAuditLog.create({
356
- data: {
357
- module: 'lawful-basis', action: 'created', entityId: record.id,
358
- entityType: 'LawfulBasisRecord', changes: { activityName, lawfulBasis, assessedBy },
359
- },
360
- });
361
- return res.status(201).json(record);
362
- });
363
-
364
- // PUT /lawful-basis/:id
365
- router.put('/:id', async (req, res) => {
366
- const { id } = req.params;
367
- const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
368
- if (!existing) return res.status(404).json({ error: 'Lawful basis record not found' });
369
- const data = { ...req.body };
370
- if (data.reviewDate) data.reviewDate = new Date(data.reviewDate);
371
- const record = await prisma.lawfulBasisRecord.update({ where: { id }, data });
372
- await prisma.complianceAuditLog.create({
373
- data: {
374
- module: 'lawful-basis', action: 'updated', entityId: record.id,
375
- entityType: 'LawfulBasisRecord', changes: data,
376
- },
377
- });
378
- return res.json(record);
379
- });
380
-
381
- // DELETE /lawful-basis/:id
382
- router.delete('/:id', async (req, res) => {
383
- const { id } = req.params;
384
- const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
385
- if (!existing) return res.status(404).json({ error: 'Lawful basis record not found' });
386
- await prisma.lawfulBasisRecord.delete({ where: { id } });
387
- await prisma.complianceAuditLog.create({
388
- data: {
389
- module: 'lawful-basis', action: 'deleted', entityId: id,
390
- entityType: 'LawfulBasisRecord', changes: { activityName: existing.activityName },
391
- },
392
- });
393
- return res.json({ success: true });
394
- });
395
-
396
- return router;
397
- }
398
-
399
- // ---------------------------------------------------------------------------
400
- // Cross-Border Transfer router — NDPA §41–42
401
- // ---------------------------------------------------------------------------
402
-
403
- function crossBorderRouter(): Router {
404
- const router = Router();
405
-
406
- // GET /cross-border?riskLevel=high&destinationCountry=US
407
- router.get('/', async (req, res) => {
408
- const { riskLevel, destinationCountry } = req.query;
409
- const where: Record<string, string> = {};
410
- if (riskLevel && typeof riskLevel === 'string') where.riskLevel = riskLevel;
411
- if (destinationCountry && typeof destinationCountry === 'string') where.destinationCountry = destinationCountry;
412
- const records = await prisma.crossBorderTransferRecord.findMany({
413
- where: Object.keys(where).length > 0 ? where : undefined,
414
- orderBy: { createdAt: 'desc' },
415
- });
416
- return res.json(records);
417
- });
418
-
419
- // GET /cross-border/:id
420
- router.get('/:id', async (req, res) => {
421
- const record = await prisma.crossBorderTransferRecord.findUnique({ where: { id: req.params.id } });
422
- if (!record) return res.status(404).json({ error: 'Cross-border transfer record not found' });
423
- return res.json(record);
424
- });
425
-
426
- // POST /cross-border
427
- router.post('/', async (req, res) => {
428
- const {
429
- destinationCountry, recipientName, transferMechanism, safeguards,
430
- dataCategories, adequacyStatus, ndpcApprovalRequired, ndpcApprovalReference, riskLevel,
431
- } = req.body;
432
- if (!destinationCountry || !recipientName || !transferMechanism || !safeguards || !dataCategories || !adequacyStatus || !riskLevel) {
433
- return res.status(400).json({
434
- error: 'destinationCountry, recipientName, transferMechanism, safeguards, dataCategories, adequacyStatus, and riskLevel are required',
435
- });
436
- }
437
- if (!Array.isArray(dataCategories)) {
438
- return res.status(400).json({ error: 'dataCategories must be an array' });
439
- }
440
- const record = await prisma.crossBorderTransferRecord.create({
441
- data: {
442
- destinationCountry, recipientName, transferMechanism, safeguards, dataCategories,
443
- adequacyStatus, ndpcApprovalRequired: ndpcApprovalRequired ?? false,
444
- ndpcApprovalReference: ndpcApprovalReference ?? null, riskLevel,
445
- },
446
- });
447
- await prisma.complianceAuditLog.create({
448
- data: {
449
- module: 'cross-border', action: 'created', entityId: record.id,
450
- entityType: 'CrossBorderTransferRecord', changes: { destinationCountry, recipientName, riskLevel },
451
- },
452
- });
453
- return res.status(201).json(record);
454
- });
455
-
456
- // PUT /cross-border/:id
457
- router.put('/:id', async (req, res) => {
458
- const { id } = req.params;
459
- const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
460
- if (!existing) return res.status(404).json({ error: 'Cross-border transfer record not found' });
461
- const record = await prisma.crossBorderTransferRecord.update({ where: { id }, data: req.body });
462
- await prisma.complianceAuditLog.create({
463
- data: {
464
- module: 'cross-border', action: 'updated', entityId: record.id,
465
- entityType: 'CrossBorderTransferRecord', changes: req.body,
466
- },
467
- });
468
- return res.json(record);
469
- });
470
-
471
- // DELETE /cross-border/:id
472
- router.delete('/:id', async (req, res) => {
473
- const { id } = req.params;
474
- const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
475
- if (!existing) return res.status(404).json({ error: 'Cross-border transfer record not found' });
476
- await prisma.crossBorderTransferRecord.delete({ where: { id } });
477
- await prisma.complianceAuditLog.create({
478
- data: {
479
- module: 'cross-border', action: 'deleted', entityId: id,
480
- entityType: 'CrossBorderTransferRecord',
481
- changes: { destinationCountry: existing.destinationCountry, recipientName: existing.recipientName },
482
- },
483
- });
484
- return res.json({ success: true });
485
- });
486
-
487
- return router;
488
- }
489
-
490
- // ---------------------------------------------------------------------------
491
- // Main factory
492
- // ---------------------------------------------------------------------------
493
-
494
- /**
495
- * Returns a fully configured NDPR compliance Express Router.
496
- *
497
- * Mount with:
498
- * app.use('/api/ndpr', createNDPRRouter());
499
- */
500
- export function createNDPRRouter(): Router {
501
- const router = Router();
502
- router.use('/consent', consentRouter());
503
- router.use('/dsr', dsrRouter());
504
- router.use('/breach', breachRouter());
505
- router.use('/dpia', dpiaRouter());
506
- router.use('/lawful-basis', lawfulBasisRouter());
507
- router.use('/cross-border', crossBorderRouter());
508
- return router;
509
- }