@tantainnovative/ndpr-recipes 0.1.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 +408 -0
- package/package.json +25 -0
- package/prisma/schema.prisma +104 -0
- package/src/adapters/drizzle-consent.ts +135 -0
- package/src/adapters/drizzle-dsr.ts +190 -0
- package/src/adapters/index.ts +39 -0
- package/src/adapters/prisma-breach.ts +181 -0
- package/src/adapters/prisma-consent.ts +129 -0
- package/src/adapters/prisma-dsr.ts +163 -0
- package/src/adapters/prisma-ropa.ts +205 -0
- package/src/drizzle/schema.ts +484 -0
- package/src/express/index.ts +82 -0
- package/src/express/middleware/consent-check.ts +133 -0
- package/src/express/routes/breach.ts +259 -0
- package/src/express/routes/compliance.ts +130 -0
- package/src/express/routes/consent.ts +163 -0
- package/src/express/routes/dsr.ts +203 -0
- package/src/express/routes/ropa.ts +225 -0
- package/src/nextjs/app-router/api/breach/[id]/route.ts +121 -0
- package/src/nextjs/app-router/api/breach/route.ts +182 -0
- package/src/nextjs/app-router/api/compliance/route.ts +162 -0
- package/src/nextjs/app-router/api/consent/route.ts +161 -0
- package/src/nextjs/app-router/api/dsr/[id]/route.ts +115 -0
- package/src/nextjs/app-router/api/dsr/route.ts +128 -0
- package/src/nextjs/app-router/api/ropa/route.ts +234 -0
- package/src/nextjs/app-router/layout-example.tsx +178 -0
- package/src/nextjs/app-router/middleware.ts +113 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express — DSR (Data Subject Rights) Router
|
|
3
|
+
*
|
|
4
|
+
* Handles listing, creating, retrieving, and updating Data Subject Rights
|
|
5
|
+
* requests as required by NDPA Sections 34–38 (rights to access, rectification,
|
|
6
|
+
* erasure, portability, and objection). Requests must be fulfilled within
|
|
7
|
+
* 30 days of submission under the Act.
|
|
8
|
+
*
|
|
9
|
+
* Routes
|
|
10
|
+
* ------
|
|
11
|
+
* GET /dsr — List DSR requests (optional ?status= filter)
|
|
12
|
+
* POST /dsr — Submit a new DSR request
|
|
13
|
+
* GET /dsr/:id — Fetch a single DSR request by ID
|
|
14
|
+
* PATCH /dsr/:id — Update request status, assignee, or internal notes
|
|
15
|
+
*
|
|
16
|
+
* How to use
|
|
17
|
+
* ----------
|
|
18
|
+
* This router is mounted automatically by `createNDPRRouter()` in `../index.ts`.
|
|
19
|
+
*
|
|
20
|
+
* @module express/routes/dsr
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Router } from 'express';
|
|
24
|
+
import { PrismaClient } from '@prisma/client';
|
|
25
|
+
|
|
26
|
+
const prisma = new PrismaClient();
|
|
27
|
+
export const dsrRouter = Router();
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// GET /dsr?status=pending
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* List all DSR requests, optionally filtered by status.
|
|
35
|
+
*
|
|
36
|
+
* Returns requests ordered newest-first so overdue items are easier to spot.
|
|
37
|
+
* Filter by status=pending to see requests awaiting acknowledgement.
|
|
38
|
+
*
|
|
39
|
+
* Query params:
|
|
40
|
+
* status (optional) — pending | in_progress | completed | rejected
|
|
41
|
+
*
|
|
42
|
+
* Returns 200 with an array of DSRRequest rows.
|
|
43
|
+
*/
|
|
44
|
+
dsrRouter.get('/', async (req, res) => {
|
|
45
|
+
const { status } = req.query;
|
|
46
|
+
|
|
47
|
+
const requests = await prisma.dSRRequest.findMany({
|
|
48
|
+
where: typeof status === 'string' ? { status } : undefined,
|
|
49
|
+
orderBy: { submittedAt: 'desc' },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return res.json(requests);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// POST /dsr
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Submit a new Data Subject Rights request.
|
|
61
|
+
*
|
|
62
|
+
* Creates a new request with status 'pending' and automatically sets the
|
|
63
|
+
* statutory 30-day deadline (dueAt) as required by NDPA Section 34.
|
|
64
|
+
* An audit log entry is created for accountability purposes.
|
|
65
|
+
*
|
|
66
|
+
* Body (JSON):
|
|
67
|
+
* type (required) — access | rectification | erasure | portability | objection
|
|
68
|
+
* subjectName (required) — full name of the data subject
|
|
69
|
+
* subjectEmail (required) — email address of the data subject
|
|
70
|
+
* identifierType (required) — how the subject is identified (e.g. 'email', 'account_id')
|
|
71
|
+
* identifierValue (required) — the subject's identifier value
|
|
72
|
+
* subjectPhone (optional) — phone number
|
|
73
|
+
* description (optional) — additional context from the subject
|
|
74
|
+
*
|
|
75
|
+
* Returns 201 with the newly created DSRRequest row.
|
|
76
|
+
*/
|
|
77
|
+
dsrRouter.post('/', async (req, res) => {
|
|
78
|
+
const {
|
|
79
|
+
type,
|
|
80
|
+
subjectName,
|
|
81
|
+
subjectEmail,
|
|
82
|
+
subjectPhone,
|
|
83
|
+
identifierType,
|
|
84
|
+
identifierValue,
|
|
85
|
+
description,
|
|
86
|
+
} = req.body;
|
|
87
|
+
|
|
88
|
+
if (!type || !subjectName || !subjectEmail || !identifierType || !identifierValue) {
|
|
89
|
+
return res.status(400).json({
|
|
90
|
+
error:
|
|
91
|
+
'type, subjectName, subjectEmail, identifierType, and identifierValue are required',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// NDPA mandates a 30-day response window from submission.
|
|
96
|
+
const dueAt = new Date();
|
|
97
|
+
dueAt.setDate(dueAt.getDate() + 30);
|
|
98
|
+
|
|
99
|
+
const request = await prisma.dSRRequest.create({
|
|
100
|
+
data: {
|
|
101
|
+
type,
|
|
102
|
+
subjectName,
|
|
103
|
+
subjectEmail,
|
|
104
|
+
subjectPhone: subjectPhone ?? null,
|
|
105
|
+
identifierType,
|
|
106
|
+
identifierValue,
|
|
107
|
+
description: description ?? null,
|
|
108
|
+
status: 'pending',
|
|
109
|
+
dueAt,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await prisma.complianceAuditLog.create({
|
|
114
|
+
data: {
|
|
115
|
+
module: 'dsr',
|
|
116
|
+
action: 'submitted',
|
|
117
|
+
entityId: request.id,
|
|
118
|
+
entityType: 'DSRRequest',
|
|
119
|
+
changes: { type, subjectEmail, status: 'pending' },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return res.status(201).json(request);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// GET /dsr/:id
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fetch a single DSR request by its ID.
|
|
132
|
+
*
|
|
133
|
+
* Returns full request details including the due date, submission timestamp,
|
|
134
|
+
* assignee, and any internal notes added by the DPO.
|
|
135
|
+
*
|
|
136
|
+
* Path params:
|
|
137
|
+
* id (required) — the DSRRequest record ID
|
|
138
|
+
*
|
|
139
|
+
* Returns 200 with the DSRRequest row, or 404 if not found.
|
|
140
|
+
*/
|
|
141
|
+
dsrRouter.get('/:id', async (req, res) => {
|
|
142
|
+
const { id } = req.params;
|
|
143
|
+
|
|
144
|
+
const request = await prisma.dSRRequest.findUnique({ where: { id } });
|
|
145
|
+
|
|
146
|
+
if (!request) {
|
|
147
|
+
return res.status(404).json({ error: 'DSR request not found' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return res.json(request);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// PATCH /dsr/:id
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update a DSR request's status, assignee, or internal notes.
|
|
159
|
+
*
|
|
160
|
+
* Used by DPO tooling to progress requests through the statutory workflow:
|
|
161
|
+
* pending → in_progress → completed (or rejected). Status transitions are
|
|
162
|
+
* timestamped so the 30-day SLA can be tracked per NDPA Section 34.
|
|
163
|
+
*
|
|
164
|
+
* Body (JSON, all fields optional):
|
|
165
|
+
* status — pending | in_progress | completed | rejected
|
|
166
|
+
* assignedTo — name/ID of the staff member handling the request
|
|
167
|
+
* internalNotes — free-text notes visible only to internal staff
|
|
168
|
+
*
|
|
169
|
+
* Returns 200 with the updated DSRRequest row.
|
|
170
|
+
*/
|
|
171
|
+
dsrRouter.patch('/:id', async (req, res) => {
|
|
172
|
+
const { id } = req.params;
|
|
173
|
+
const { status, assignedTo, internalNotes } = req.body;
|
|
174
|
+
|
|
175
|
+
const data: Record<string, unknown> = {};
|
|
176
|
+
if (status !== undefined) {
|
|
177
|
+
data.status = status;
|
|
178
|
+
if (status === 'in_progress') data.acknowledgedAt = new Date();
|
|
179
|
+
if (status === 'completed') data.completedAt = new Date();
|
|
180
|
+
}
|
|
181
|
+
if (assignedTo !== undefined) data.assignedTo = assignedTo;
|
|
182
|
+
if (internalNotes !== undefined) data.internalNotes = internalNotes;
|
|
183
|
+
|
|
184
|
+
if (Object.keys(data).length === 0) {
|
|
185
|
+
return res.status(400).json({
|
|
186
|
+
error: 'At least one of status, assignedTo, or internalNotes must be provided',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const updated = await prisma.dSRRequest.update({ where: { id }, data });
|
|
191
|
+
|
|
192
|
+
await prisma.complianceAuditLog.create({
|
|
193
|
+
data: {
|
|
194
|
+
module: 'dsr',
|
|
195
|
+
action: 'updated',
|
|
196
|
+
entityId: id,
|
|
197
|
+
entityType: 'DSRRequest',
|
|
198
|
+
changes: data as any,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return res.json(updated);
|
|
203
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express — ROPA (Record of Processing Activities) Router
|
|
3
|
+
*
|
|
4
|
+
* Handles listing, creating, updating, and archiving processing activity records.
|
|
5
|
+
* Under the NDPA accountability principle, data controllers must maintain a
|
|
6
|
+
* Record of Processing Activities (ROPA). This router provides the full CRUD
|
|
7
|
+
* surface for managing that record.
|
|
8
|
+
*
|
|
9
|
+
* Routes
|
|
10
|
+
* ------
|
|
11
|
+
* GET /ropa — List all processing records (optional ?status= filter)
|
|
12
|
+
* POST /ropa — Create a new processing record
|
|
13
|
+
* PATCH /ropa — Update a processing record (body must include `id`)
|
|
14
|
+
* DELETE /ropa?id=xxx — Archive a processing record (soft delete)
|
|
15
|
+
*
|
|
16
|
+
* How to use
|
|
17
|
+
* ----------
|
|
18
|
+
* This router is mounted automatically by `createNDPRRouter()` in `../index.ts`.
|
|
19
|
+
*
|
|
20
|
+
* @module express/routes/ropa
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Router } from 'express';
|
|
24
|
+
import { PrismaClient } from '@prisma/client';
|
|
25
|
+
|
|
26
|
+
const prisma = new PrismaClient();
|
|
27
|
+
export const ropaRouter = Router();
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// GET /ropa?status=active
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* List all processing activity records.
|
|
35
|
+
*
|
|
36
|
+
* Returns records ordered by creation date ascending so the ROPA reads
|
|
37
|
+
* chronologically. Filter by status=active to see only current activities;
|
|
38
|
+
* status=archived to review historical entries retained for audit purposes.
|
|
39
|
+
*
|
|
40
|
+
* Query params:
|
|
41
|
+
* status (optional) — active | archived
|
|
42
|
+
*
|
|
43
|
+
* Returns 200 with an array of ProcessingRecord rows.
|
|
44
|
+
*/
|
|
45
|
+
ropaRouter.get('/', async (req, res) => {
|
|
46
|
+
const { status } = req.query;
|
|
47
|
+
|
|
48
|
+
const records = await prisma.processingRecord.findMany({
|
|
49
|
+
where: typeof status === 'string' ? { status } : undefined,
|
|
50
|
+
orderBy: { createdAt: 'asc' },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return res.json(records);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// POST /ropa
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a new processing activity record.
|
|
62
|
+
*
|
|
63
|
+
* Each record documents a single processing activity (e.g. "Customer order
|
|
64
|
+
* processing" or "Marketing email campaigns"). Together, all active records
|
|
65
|
+
* form the organisation's ROPA required under the NDPA accountability principle.
|
|
66
|
+
*
|
|
67
|
+
* Body (JSON):
|
|
68
|
+
* purpose (required) — description of the processing activity
|
|
69
|
+
* lawfulBasis (required) — consent | contract | legal_obligation |
|
|
70
|
+
* vital_interests | public_task | legitimate_interests
|
|
71
|
+
* dataCategories (required) — array of data category labels
|
|
72
|
+
* dataSubjects (required) — array of subject category labels
|
|
73
|
+
* recipients (required) — array of recipient category labels
|
|
74
|
+
* retentionPeriod (required) — human-readable retention policy (e.g. '7 years')
|
|
75
|
+
* securityMeasures (required) — array of security measures in place
|
|
76
|
+
* transferCountries (optional) — array of countries receiving cross-border transfers
|
|
77
|
+
* transferMechanism (optional) — legal mechanism for the transfers
|
|
78
|
+
* dpiaConducted (optional) — whether a DPIA has been performed (default false)
|
|
79
|
+
*
|
|
80
|
+
* Returns 201 with the newly created ProcessingRecord row.
|
|
81
|
+
*/
|
|
82
|
+
ropaRouter.post('/', async (req, res) => {
|
|
83
|
+
const {
|
|
84
|
+
purpose,
|
|
85
|
+
lawfulBasis,
|
|
86
|
+
dataCategories,
|
|
87
|
+
dataSubjects,
|
|
88
|
+
recipients,
|
|
89
|
+
retentionPeriod,
|
|
90
|
+
securityMeasures,
|
|
91
|
+
transferCountries,
|
|
92
|
+
transferMechanism,
|
|
93
|
+
dpiaConducted,
|
|
94
|
+
} = req.body;
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
!purpose ||
|
|
98
|
+
!lawfulBasis ||
|
|
99
|
+
!Array.isArray(dataCategories) ||
|
|
100
|
+
!Array.isArray(dataSubjects) ||
|
|
101
|
+
!Array.isArray(recipients) ||
|
|
102
|
+
!retentionPeriod ||
|
|
103
|
+
!Array.isArray(securityMeasures)
|
|
104
|
+
) {
|
|
105
|
+
return res.status(400).json({
|
|
106
|
+
error:
|
|
107
|
+
'purpose, lawfulBasis, dataCategories, dataSubjects, recipients, retentionPeriod, and securityMeasures are required',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const record = await prisma.processingRecord.create({
|
|
112
|
+
data: {
|
|
113
|
+
purpose,
|
|
114
|
+
lawfulBasis,
|
|
115
|
+
dataCategories,
|
|
116
|
+
dataSubjects,
|
|
117
|
+
recipients,
|
|
118
|
+
retentionPeriod,
|
|
119
|
+
securityMeasures,
|
|
120
|
+
transferCountries: transferCountries ?? null,
|
|
121
|
+
transferMechanism: transferMechanism ?? null,
|
|
122
|
+
dpiaConducted: dpiaConducted ?? false,
|
|
123
|
+
status: 'active',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await prisma.complianceAuditLog.create({
|
|
128
|
+
data: {
|
|
129
|
+
module: 'ropa',
|
|
130
|
+
action: 'created',
|
|
131
|
+
entityId: record.id,
|
|
132
|
+
entityType: 'ProcessingRecord',
|
|
133
|
+
changes: { purpose, lawfulBasis, status: 'active' },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return res.status(201).json(record);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// PATCH /ropa
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Update an existing processing activity record.
|
|
146
|
+
*
|
|
147
|
+
* Call this when a processing activity changes — e.g. a new data category
|
|
148
|
+
* is added, the retention policy is updated, or a DPIA is conducted.
|
|
149
|
+
* Keeping the ROPA accurate and current is part of the NDPA accountability obligation.
|
|
150
|
+
*
|
|
151
|
+
* Body (JSON):
|
|
152
|
+
* id (required) — ID of the ProcessingRecord to update
|
|
153
|
+
* ... (all other POST fields are optional — only include what changed)
|
|
154
|
+
*
|
|
155
|
+
* Returns 200 with the updated ProcessingRecord row.
|
|
156
|
+
*/
|
|
157
|
+
ropaRouter.patch('/', async (req, res) => {
|
|
158
|
+
const { id, ...fields } = req.body;
|
|
159
|
+
|
|
160
|
+
if (!id) {
|
|
161
|
+
return res.status(400).json({ error: 'id is required in the request body' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const existing = await prisma.processingRecord.findUnique({ where: { id } });
|
|
165
|
+
if (!existing) {
|
|
166
|
+
return res.status(404).json({ error: 'Processing record not found' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const updated = await prisma.processingRecord.update({ where: { id }, data: fields });
|
|
170
|
+
|
|
171
|
+
await prisma.complianceAuditLog.create({
|
|
172
|
+
data: {
|
|
173
|
+
module: 'ropa',
|
|
174
|
+
action: 'updated',
|
|
175
|
+
entityId: id,
|
|
176
|
+
entityType: 'ProcessingRecord',
|
|
177
|
+
changes: fields,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return res.json(updated);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// DELETE /ropa?id=xxx
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Archive a processing activity record (soft delete).
|
|
190
|
+
*
|
|
191
|
+
* Records are never hard-deleted — archiving sets status to 'archived' so
|
|
192
|
+
* the historical ROPA remains available for regulatory review. This supports
|
|
193
|
+
* the NDPA accountability principle requirement to demonstrate compliance
|
|
194
|
+
* over time, not just at a single point in time.
|
|
195
|
+
*
|
|
196
|
+
* Query params:
|
|
197
|
+
* id (required) — ID of the ProcessingRecord to archive
|
|
198
|
+
*
|
|
199
|
+
* Returns 200 `{ success: true }` when complete.
|
|
200
|
+
*/
|
|
201
|
+
ropaRouter.delete('/', async (req, res) => {
|
|
202
|
+
const { id } = req.query;
|
|
203
|
+
|
|
204
|
+
if (!id || typeof id !== 'string') {
|
|
205
|
+
return res.status(400).json({ error: 'id query parameter required' });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const existing = await prisma.processingRecord.findUnique({ where: { id } });
|
|
209
|
+
if (!existing) {
|
|
210
|
+
return res.status(404).json({ error: 'Processing record not found' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await prisma.processingRecord.update({ where: { id }, data: { status: 'archived' } });
|
|
214
|
+
|
|
215
|
+
await prisma.complianceAuditLog.create({
|
|
216
|
+
data: {
|
|
217
|
+
module: 'ropa',
|
|
218
|
+
action: 'archived',
|
|
219
|
+
entityId: id,
|
|
220
|
+
entityType: 'ProcessingRecord',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return res.json({ success: true });
|
|
225
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router — Breach Notification Single-Report Route
|
|
3
|
+
*
|
|
4
|
+
* Handles reading and updating individual breach reports.
|
|
5
|
+
* Used by the DPO interface to track an incident through its lifecycle
|
|
6
|
+
* and record the NDPC notification status as required by NDPA Section 40.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints
|
|
9
|
+
* ---------
|
|
10
|
+
* GET /api/breach/[id] — Fetch a single breach report by ID
|
|
11
|
+
* PATCH /api/breach/[id] — Update breach status, severity, or add actions taken
|
|
12
|
+
*
|
|
13
|
+
* How to use
|
|
14
|
+
* ----------
|
|
15
|
+
* Copy this file to `app/api/breach/[id]/route.ts` in your Next.js project.
|
|
16
|
+
*
|
|
17
|
+
* @module breach/[id]/route
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
21
|
+
import { PrismaClient } from '@prisma/client';
|
|
22
|
+
|
|
23
|
+
const prisma = new PrismaClient();
|
|
24
|
+
|
|
25
|
+
/** Next.js App Router route context — contains dynamic segment params */
|
|
26
|
+
interface RouteContext {
|
|
27
|
+
params: { id: string };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// GET /api/breach/[id]
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch a single breach report by its ID.
|
|
36
|
+
*
|
|
37
|
+
* Returns the full report including affected systems, data types, and the
|
|
38
|
+
* NDPC notification status. Used by the incident detail page and for
|
|
39
|
+
* generating the formal NDPC breach notification document.
|
|
40
|
+
*
|
|
41
|
+
* Path params:
|
|
42
|
+
* id (required) — the BreachReport record ID
|
|
43
|
+
*
|
|
44
|
+
* Returns 200 with the BreachReport row, or 404 if not found.
|
|
45
|
+
*/
|
|
46
|
+
export async function GET(_req: NextRequest, { params }: RouteContext) {
|
|
47
|
+
const { id } = params;
|
|
48
|
+
|
|
49
|
+
const report = await prisma.breachReport.findUnique({ where: { id } });
|
|
50
|
+
|
|
51
|
+
if (!report) {
|
|
52
|
+
return NextResponse.json({ error: 'Breach report not found' }, { status: 404 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return NextResponse.json(report);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// PATCH /api/breach/[id]
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update a breach report's status, severity, NDPC notification status, or
|
|
64
|
+
* containment actions.
|
|
65
|
+
*
|
|
66
|
+
* Key workflow transitions tracked here:
|
|
67
|
+
* ongoing → investigating (DPO has begun assessment)
|
|
68
|
+
* investigating → resolved (threat contained, remediation complete)
|
|
69
|
+
* resolved → closed (post-incident review done, NDPC notified)
|
|
70
|
+
*
|
|
71
|
+
* The ndpcNotificationSent flag and ndpcNotifiedAt timestamp are set here
|
|
72
|
+
* once the formal NDPC notification has been dispatched, fulfilling the
|
|
73
|
+
* 72-hour reporting obligation under NDPA Section 40.
|
|
74
|
+
*
|
|
75
|
+
* Body (JSON, all fields optional):
|
|
76
|
+
* status — ongoing | investigating | resolved | closed
|
|
77
|
+
* severity — critical | high | medium | low
|
|
78
|
+
* initialActions — append/replace containment actions text
|
|
79
|
+
* ndpcNotificationSent — boolean — set true once NDPC is formally notified
|
|
80
|
+
*
|
|
81
|
+
* Returns 200 with the updated BreachReport row.
|
|
82
|
+
*/
|
|
83
|
+
export async function PATCH(req: NextRequest, { params }: RouteContext) {
|
|
84
|
+
const { id } = params;
|
|
85
|
+
const body = await req.json();
|
|
86
|
+
const { status, severity, initialActions, ndpcNotificationSent } = body;
|
|
87
|
+
|
|
88
|
+
const data: Record<string, unknown> = {};
|
|
89
|
+
|
|
90
|
+
if (status !== undefined) data.status = status;
|
|
91
|
+
if (severity !== undefined) data.severity = severity;
|
|
92
|
+
if (initialActions !== undefined) data.initialActions = initialActions;
|
|
93
|
+
|
|
94
|
+
// Stamp the NDPC notification timestamp when the flag is first set to true.
|
|
95
|
+
if (ndpcNotificationSent === true) {
|
|
96
|
+
data.ndpcNotificationSent = true;
|
|
97
|
+
data.ndpcNotifiedAt = new Date();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Object.keys(data).length === 0) {
|
|
101
|
+
return NextResponse.json(
|
|
102
|
+
{ error: 'At least one of status, severity, initialActions, or ndpcNotificationSent must be provided' },
|
|
103
|
+
{ status: 400 },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const updated = await prisma.breachReport.update({ where: { id }, data });
|
|
108
|
+
|
|
109
|
+
// Audit log — every status change on a breach is significant for compliance.
|
|
110
|
+
await prisma.complianceAuditLog.create({
|
|
111
|
+
data: {
|
|
112
|
+
module: 'breach',
|
|
113
|
+
action: 'updated',
|
|
114
|
+
entityId: id,
|
|
115
|
+
entityType: 'BreachReport',
|
|
116
|
+
changes: data as any,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return NextResponse.json(updated);
|
|
121
|
+
}
|