@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.
@@ -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
+ }