@tantainnovative/create-ndpr 0.1.0 → 0.4.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 +28 -5
- package/bin/index.mjs +146 -11
- package/package.json +14 -4
- package/templates/drizzle-schema.ts +92 -7
- package/templates/express-consent-route.ts +111 -5
- package/templates/express-cross-border-route.ts +317 -0
- package/templates/express-dpia-route.ts +287 -0
- package/templates/express-lawful-basis-route.ts +292 -0
- package/templates/express-setup.ts +262 -3
- package/templates/github-ndpr-audit.yml +31 -0
- package/templates/ndpr-audit.json +14 -0
- package/templates/nextjs-breach-route.ts +133 -1
- package/templates/nextjs-consent-route.ts +93 -28
- package/templates/nextjs-cross-border-route.ts +325 -0
- package/templates/nextjs-dpia-route.ts +290 -0
- package/templates/nextjs-dsr-route.ts +84 -3
- package/templates/nextjs-lawful-basis-route.ts +297 -0
- package/templates/nextjs-layout.tsx +12 -3
- package/templates/prisma-schema.prisma +63 -5
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router — DPIA (Data Protection Impact Assessment) Route
|
|
3
|
+
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
|
+
*
|
|
5
|
+
* NDPA §28 — a DPIA must be conducted where processing is likely
|
|
6
|
+
* to result in a high risk to the rights and freedoms of data subjects.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints:
|
|
9
|
+
* GET /api/dpia — List DPIA records (optional ?status= filter)
|
|
10
|
+
* GET /api/dpia?id=xxx — Get a single DPIA record
|
|
11
|
+
* POST /api/dpia — Create a new DPIA record
|
|
12
|
+
* PUT /api/dpia — Update an existing DPIA record
|
|
13
|
+
* DELETE /api/dpia?id=xxx — Delete a DPIA record
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
17
|
+
// {{#if ORM=prisma}}
|
|
18
|
+
import { PrismaClient } from '@prisma/client';
|
|
19
|
+
|
|
20
|
+
const prisma = new PrismaClient();
|
|
21
|
+
// {{/if}}
|
|
22
|
+
// {{#if ORM=drizzle}}
|
|
23
|
+
import { db } from '@/drizzle';
|
|
24
|
+
import { dpiaRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
|
|
25
|
+
import { eq, desc } from 'drizzle-orm';
|
|
26
|
+
// {{/if}}
|
|
27
|
+
// {{#if ORM=none}}
|
|
28
|
+
// TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
|
|
29
|
+
// in-memory map below is enough to develop against locally but DOES NOT
|
|
30
|
+
// satisfy NDPA Section 44 (record-keeping) or the auditability NDPC asks
|
|
31
|
+
// about. Replace `dpiaStore`/`auditLog` with your DB / KV / API.
|
|
32
|
+
interface DPIARecord {
|
|
33
|
+
id: string;
|
|
34
|
+
projectName: string;
|
|
35
|
+
description: string;
|
|
36
|
+
dpiaData: unknown;
|
|
37
|
+
overallRisk: string;
|
|
38
|
+
score: number;
|
|
39
|
+
status: string;
|
|
40
|
+
conductedBy: string;
|
|
41
|
+
approvedBy: string | null;
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
updatedAt: Date;
|
|
44
|
+
}
|
|
45
|
+
const dpiaStore = new Map<string, DPIARecord>();
|
|
46
|
+
const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
|
|
47
|
+
function newId() { return crypto.randomUUID(); }
|
|
48
|
+
// {{/if}}
|
|
49
|
+
|
|
50
|
+
// GET /api/dpia?status=draft OR /api/dpia?id=xxx
|
|
51
|
+
export async function GET(req: NextRequest) {
|
|
52
|
+
const id = req.nextUrl.searchParams.get('id');
|
|
53
|
+
|
|
54
|
+
if (id) {
|
|
55
|
+
// {{#if ORM=prisma}}
|
|
56
|
+
const record = await prisma.dPIARecord.findUnique({ where: { id } });
|
|
57
|
+
// {{/if}}
|
|
58
|
+
// {{#if ORM=drizzle}}
|
|
59
|
+
const [record] = await db.select().from(dpiaRecords).where(eq(dpiaRecords.id, id)).limit(1);
|
|
60
|
+
// {{/if}}
|
|
61
|
+
// {{#if ORM=none}}
|
|
62
|
+
const record = dpiaStore.get(id) ?? null;
|
|
63
|
+
// {{/if}}
|
|
64
|
+
|
|
65
|
+
if (!record) {
|
|
66
|
+
return NextResponse.json({ error: 'DPIA record not found' }, { status: 404 });
|
|
67
|
+
}
|
|
68
|
+
return NextResponse.json(record);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const status = req.nextUrl.searchParams.get('status');
|
|
72
|
+
|
|
73
|
+
// {{#if ORM=prisma}}
|
|
74
|
+
const records = await prisma.dPIARecord.findMany({
|
|
75
|
+
where: status ? { status } : undefined,
|
|
76
|
+
orderBy: { createdAt: 'desc' },
|
|
77
|
+
});
|
|
78
|
+
// {{/if}}
|
|
79
|
+
// {{#if ORM=drizzle}}
|
|
80
|
+
const records = status
|
|
81
|
+
? await db.select().from(dpiaRecords).where(eq(dpiaRecords.status, status)).orderBy(desc(dpiaRecords.createdAt))
|
|
82
|
+
: await db.select().from(dpiaRecords).orderBy(desc(dpiaRecords.createdAt));
|
|
83
|
+
// {{/if}}
|
|
84
|
+
// {{#if ORM=none}}
|
|
85
|
+
const records = [...dpiaStore.values()]
|
|
86
|
+
.filter((r) => (status ? r.status === status : true))
|
|
87
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
88
|
+
// {{/if}}
|
|
89
|
+
|
|
90
|
+
return NextResponse.json(records);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// POST /api/dpia
|
|
94
|
+
export async function POST(req: NextRequest) {
|
|
95
|
+
const body = await req.json();
|
|
96
|
+
const {
|
|
97
|
+
projectName,
|
|
98
|
+
description,
|
|
99
|
+
dpiaData,
|
|
100
|
+
overallRisk,
|
|
101
|
+
score,
|
|
102
|
+
conductedBy,
|
|
103
|
+
approvedBy,
|
|
104
|
+
} = body;
|
|
105
|
+
|
|
106
|
+
if (!projectName || !description || !dpiaData || !overallRisk || score == null || !conductedBy) {
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{ error: 'projectName, description, dpiaData, overallRisk, score, and conductedBy are required' },
|
|
109
|
+
{ status: 400 },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// {{#if ORM=prisma}}
|
|
114
|
+
const record = await prisma.dPIARecord.create({
|
|
115
|
+
data: {
|
|
116
|
+
projectName,
|
|
117
|
+
description,
|
|
118
|
+
dpiaData,
|
|
119
|
+
overallRisk,
|
|
120
|
+
score,
|
|
121
|
+
status: 'draft',
|
|
122
|
+
conductedBy,
|
|
123
|
+
approvedBy: approvedBy ?? null,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await prisma.complianceAuditLog.create({
|
|
128
|
+
data: {
|
|
129
|
+
module: 'dpia',
|
|
130
|
+
action: 'created',
|
|
131
|
+
entityId: record.id,
|
|
132
|
+
entityType: 'DPIARecord',
|
|
133
|
+
changes: { projectName, overallRisk, score, status: 'draft' },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
// {{/if}}
|
|
137
|
+
// {{#if ORM=drizzle}}
|
|
138
|
+
const [record] = await db.insert(dpiaRecords).values({
|
|
139
|
+
projectName,
|
|
140
|
+
description,
|
|
141
|
+
dpiaData,
|
|
142
|
+
overallRisk,
|
|
143
|
+
score,
|
|
144
|
+
status: 'draft',
|
|
145
|
+
conductedBy,
|
|
146
|
+
approvedBy: approvedBy ?? null,
|
|
147
|
+
}).returning();
|
|
148
|
+
|
|
149
|
+
await db.insert(complianceAuditLog).values({
|
|
150
|
+
module: 'dpia',
|
|
151
|
+
action: 'created',
|
|
152
|
+
entityId: record.id,
|
|
153
|
+
entityType: 'DPIARecord',
|
|
154
|
+
changes: { projectName, overallRisk, score, status: 'draft' },
|
|
155
|
+
});
|
|
156
|
+
// {{/if}}
|
|
157
|
+
// {{#if ORM=none}}
|
|
158
|
+
const now = new Date();
|
|
159
|
+
const record: DPIARecord = {
|
|
160
|
+
id: newId(),
|
|
161
|
+
projectName,
|
|
162
|
+
description,
|
|
163
|
+
dpiaData,
|
|
164
|
+
overallRisk,
|
|
165
|
+
score,
|
|
166
|
+
status: 'draft',
|
|
167
|
+
conductedBy,
|
|
168
|
+
approvedBy: approvedBy ?? null,
|
|
169
|
+
createdAt: now,
|
|
170
|
+
updatedAt: now,
|
|
171
|
+
};
|
|
172
|
+
dpiaStore.set(record.id, record);
|
|
173
|
+
auditLog.push({ id: newId(), module: 'dpia', action: 'created', entityId: record.id, at: new Date() });
|
|
174
|
+
// {{/if}}
|
|
175
|
+
|
|
176
|
+
return NextResponse.json(record, { status: 201 });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// PUT /api/dpia
|
|
180
|
+
export async function PUT(req: NextRequest) {
|
|
181
|
+
const body = await req.json();
|
|
182
|
+
const { id, ...data } = body;
|
|
183
|
+
|
|
184
|
+
if (!id) {
|
|
185
|
+
return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// {{#if ORM=prisma}}
|
|
189
|
+
const existing = await prisma.dPIARecord.findUnique({ where: { id } });
|
|
190
|
+
if (!existing) {
|
|
191
|
+
return NextResponse.json({ error: 'DPIA record not found' }, { status: 404 });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const record = await prisma.dPIARecord.update({
|
|
195
|
+
where: { id },
|
|
196
|
+
data,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await prisma.complianceAuditLog.create({
|
|
200
|
+
data: {
|
|
201
|
+
module: 'dpia',
|
|
202
|
+
action: 'updated',
|
|
203
|
+
entityId: record.id,
|
|
204
|
+
entityType: 'DPIARecord',
|
|
205
|
+
changes: data,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
// {{/if}}
|
|
209
|
+
// {{#if ORM=drizzle}}
|
|
210
|
+
const [existing] = await db.select().from(dpiaRecords).where(eq(dpiaRecords.id, id)).limit(1);
|
|
211
|
+
if (!existing) {
|
|
212
|
+
return NextResponse.json({ error: 'DPIA record not found' }, { status: 404 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const [record] = await db.update(dpiaRecords).set(data).where(eq(dpiaRecords.id, id)).returning();
|
|
216
|
+
|
|
217
|
+
await db.insert(complianceAuditLog).values({
|
|
218
|
+
module: 'dpia',
|
|
219
|
+
action: 'updated',
|
|
220
|
+
entityId: record.id,
|
|
221
|
+
entityType: 'DPIARecord',
|
|
222
|
+
changes: data,
|
|
223
|
+
});
|
|
224
|
+
// {{/if}}
|
|
225
|
+
// {{#if ORM=none}}
|
|
226
|
+
const existing = dpiaStore.get(id);
|
|
227
|
+
if (!existing) {
|
|
228
|
+
return NextResponse.json({ error: 'DPIA record not found' }, { status: 404 });
|
|
229
|
+
}
|
|
230
|
+
const record: DPIARecord = { ...existing, ...data, id, updatedAt: new Date() };
|
|
231
|
+
dpiaStore.set(id, record);
|
|
232
|
+
auditLog.push({ id: newId(), module: 'dpia', action: 'updated', entityId: record.id, at: new Date() });
|
|
233
|
+
// {{/if}}
|
|
234
|
+
|
|
235
|
+
return NextResponse.json(record);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// DELETE /api/dpia?id=xxx
|
|
239
|
+
export async function DELETE(req: NextRequest) {
|
|
240
|
+
const id = req.nextUrl.searchParams.get('id');
|
|
241
|
+
|
|
242
|
+
if (!id) {
|
|
243
|
+
return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// {{#if ORM=prisma}}
|
|
247
|
+
const existing = await prisma.dPIARecord.findUnique({ where: { id } });
|
|
248
|
+
if (!existing) {
|
|
249
|
+
return NextResponse.json({ error: 'DPIA record not found' }, { status: 404 });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await prisma.dPIARecord.delete({ where: { id } });
|
|
253
|
+
|
|
254
|
+
await prisma.complianceAuditLog.create({
|
|
255
|
+
data: {
|
|
256
|
+
module: 'dpia',
|
|
257
|
+
action: 'deleted',
|
|
258
|
+
entityId: id,
|
|
259
|
+
entityType: 'DPIARecord',
|
|
260
|
+
changes: { projectName: existing.projectName },
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
// {{/if}}
|
|
264
|
+
// {{#if ORM=drizzle}}
|
|
265
|
+
const [existing] = await db.select().from(dpiaRecords).where(eq(dpiaRecords.id, id)).limit(1);
|
|
266
|
+
if (!existing) {
|
|
267
|
+
return NextResponse.json({ error: 'DPIA record not found' }, { status: 404 });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await db.delete(dpiaRecords).where(eq(dpiaRecords.id, id));
|
|
271
|
+
|
|
272
|
+
await db.insert(complianceAuditLog).values({
|
|
273
|
+
module: 'dpia',
|
|
274
|
+
action: 'deleted',
|
|
275
|
+
entityId: id,
|
|
276
|
+
entityType: 'DPIARecord',
|
|
277
|
+
changes: { projectName: existing.projectName },
|
|
278
|
+
});
|
|
279
|
+
// {{/if}}
|
|
280
|
+
// {{#if ORM=none}}
|
|
281
|
+
const existing = dpiaStore.get(id);
|
|
282
|
+
if (!existing) {
|
|
283
|
+
return NextResponse.json({ error: 'DPIA record not found' }, { status: 404 });
|
|
284
|
+
}
|
|
285
|
+
dpiaStore.delete(id);
|
|
286
|
+
auditLog.push({ id: newId(), module: 'dpia', action: 'deleted', entityId: id, at: new Date() });
|
|
287
|
+
// {{/if}}
|
|
288
|
+
|
|
289
|
+
return NextResponse.json({ success: true });
|
|
290
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Next.js App Router — DSR (Data Subject Rights) Route
|
|
3
3
|
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
4
|
*
|
|
5
|
-
* NDPA
|
|
6
|
-
*
|
|
7
|
-
* within
|
|
5
|
+
* NDPA §34 — rights to access, rectification, erasure, portability, and
|
|
6
|
+
* objection. All requests must be acknowledged promptly and fulfilled
|
|
7
|
+
* within the statutory window (NDPC guidance: 30 days).
|
|
8
8
|
*
|
|
9
9
|
* Endpoints:
|
|
10
10
|
* GET /api/dsr — List DSR requests (optional ?status= filter)
|
|
@@ -12,18 +12,59 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { NextRequest, NextResponse } from 'next/server';
|
|
15
|
+
// {{#if ORM=prisma}}
|
|
15
16
|
import { PrismaClient } from '@prisma/client';
|
|
16
17
|
|
|
17
18
|
const prisma = new PrismaClient();
|
|
19
|
+
// {{/if}}
|
|
20
|
+
// {{#if ORM=drizzle}}
|
|
21
|
+
import { db } from '@/drizzle';
|
|
22
|
+
import { dsrRequests, complianceAuditLog } from '@/drizzle/ndpr-schema';
|
|
23
|
+
import { eq, desc } from 'drizzle-orm';
|
|
24
|
+
// {{/if}}
|
|
25
|
+
// {{#if ORM=none}}
|
|
26
|
+
// TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
|
|
27
|
+
// in-memory map below is enough to develop against locally but DOES NOT
|
|
28
|
+
// satisfy NDPA Section 44 (record-keeping) or the auditability NDPC asks
|
|
29
|
+
// about. Replace `dsrStore`/`auditLog` with your DB / KV / API.
|
|
30
|
+
interface DSRRequest {
|
|
31
|
+
id: string;
|
|
32
|
+
type: string;
|
|
33
|
+
subjectName: string;
|
|
34
|
+
subjectEmail: string;
|
|
35
|
+
subjectPhone: string | null;
|
|
36
|
+
identifierType: string;
|
|
37
|
+
identifierValue: string;
|
|
38
|
+
description: string | null;
|
|
39
|
+
status: string;
|
|
40
|
+
submittedAt: Date;
|
|
41
|
+
dueAt: Date;
|
|
42
|
+
}
|
|
43
|
+
const dsrStore = new Map<string, DSRRequest>();
|
|
44
|
+
const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
|
|
45
|
+
function newId() { return crypto.randomUUID(); }
|
|
46
|
+
// {{/if}}
|
|
18
47
|
|
|
19
48
|
// GET /api/dsr?status=pending
|
|
20
49
|
export async function GET(req: NextRequest) {
|
|
21
50
|
const status = req.nextUrl.searchParams.get('status');
|
|
22
51
|
|
|
52
|
+
// {{#if ORM=prisma}}
|
|
23
53
|
const requests = await prisma.dSRRequest.findMany({
|
|
24
54
|
where: status ? { status } : undefined,
|
|
25
55
|
orderBy: { submittedAt: 'desc' },
|
|
26
56
|
});
|
|
57
|
+
// {{/if}}
|
|
58
|
+
// {{#if ORM=drizzle}}
|
|
59
|
+
const requests = status
|
|
60
|
+
? await db.select().from(dsrRequests).where(eq(dsrRequests.status, status)).orderBy(desc(dsrRequests.submittedAt))
|
|
61
|
+
: await db.select().from(dsrRequests).orderBy(desc(dsrRequests.submittedAt));
|
|
62
|
+
// {{/if}}
|
|
63
|
+
// {{#if ORM=none}}
|
|
64
|
+
const requests = [...dsrStore.values()]
|
|
65
|
+
.filter((r) => (status ? r.status === status : true))
|
|
66
|
+
.sort((a, b) => b.submittedAt.getTime() - a.submittedAt.getTime());
|
|
67
|
+
// {{/if}}
|
|
27
68
|
|
|
28
69
|
return NextResponse.json(requests);
|
|
29
70
|
}
|
|
@@ -52,6 +93,7 @@ export async function POST(req: NextRequest) {
|
|
|
52
93
|
const dueAt = new Date();
|
|
53
94
|
dueAt.setDate(dueAt.getDate() + 30);
|
|
54
95
|
|
|
96
|
+
// {{#if ORM=prisma}}
|
|
55
97
|
const request = await prisma.dSRRequest.create({
|
|
56
98
|
data: {
|
|
57
99
|
type,
|
|
@@ -75,6 +117,45 @@ export async function POST(req: NextRequest) {
|
|
|
75
117
|
changes: { type, subjectEmail, status: 'pending' },
|
|
76
118
|
},
|
|
77
119
|
});
|
|
120
|
+
// {{/if}}
|
|
121
|
+
// {{#if ORM=drizzle}}
|
|
122
|
+
const [request] = await db.insert(dsrRequests).values({
|
|
123
|
+
type,
|
|
124
|
+
subjectName,
|
|
125
|
+
subjectEmail,
|
|
126
|
+
subjectPhone: subjectPhone ?? null,
|
|
127
|
+
identifierType,
|
|
128
|
+
identifierValue,
|
|
129
|
+
description: description ?? null,
|
|
130
|
+
status: 'pending',
|
|
131
|
+
dueAt,
|
|
132
|
+
}).returning();
|
|
133
|
+
|
|
134
|
+
await db.insert(complianceAuditLog).values({
|
|
135
|
+
module: 'dsr',
|
|
136
|
+
action: 'submitted',
|
|
137
|
+
entityId: request.id,
|
|
138
|
+
entityType: 'DSRRequest',
|
|
139
|
+
changes: { type, subjectEmail, status: 'pending' },
|
|
140
|
+
});
|
|
141
|
+
// {{/if}}
|
|
142
|
+
// {{#if ORM=none}}
|
|
143
|
+
const request: DSRRequest = {
|
|
144
|
+
id: newId(),
|
|
145
|
+
type,
|
|
146
|
+
subjectName,
|
|
147
|
+
subjectEmail,
|
|
148
|
+
subjectPhone: subjectPhone ?? null,
|
|
149
|
+
identifierType,
|
|
150
|
+
identifierValue,
|
|
151
|
+
description: description ?? null,
|
|
152
|
+
status: 'pending',
|
|
153
|
+
submittedAt: new Date(),
|
|
154
|
+
dueAt,
|
|
155
|
+
};
|
|
156
|
+
dsrStore.set(request.id, request);
|
|
157
|
+
auditLog.push({ id: newId(), module: 'dsr', action: 'submitted', entityId: request.id, at: new Date() });
|
|
158
|
+
// {{/if}}
|
|
78
159
|
|
|
79
160
|
return NextResponse.json(request, { status: 201 });
|
|
80
161
|
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router — Lawful Basis Route
|
|
3
|
+
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
|
+
*
|
|
5
|
+
* NDPA §25 — data processing must have a lawful basis. This route
|
|
6
|
+
* manages a register of processing activities and their lawful bases.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints:
|
|
9
|
+
* GET /api/lawful-basis — List lawful basis records (optional ?lawfulBasis= filter)
|
|
10
|
+
* GET /api/lawful-basis?id=xxx — Get a single record
|
|
11
|
+
* POST /api/lawful-basis — Create a new lawful basis record
|
|
12
|
+
* PUT /api/lawful-basis — Update an existing record
|
|
13
|
+
* DELETE /api/lawful-basis?id=xxx — Delete a record
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
17
|
+
// {{#if ORM=prisma}}
|
|
18
|
+
import { PrismaClient } from '@prisma/client';
|
|
19
|
+
|
|
20
|
+
const prisma = new PrismaClient();
|
|
21
|
+
// {{/if}}
|
|
22
|
+
// {{#if ORM=drizzle}}
|
|
23
|
+
import { db } from '@/drizzle';
|
|
24
|
+
import { lawfulBasisRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
|
|
25
|
+
import { eq, desc } from 'drizzle-orm';
|
|
26
|
+
// {{/if}}
|
|
27
|
+
// {{#if ORM=none}}
|
|
28
|
+
// TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
|
|
29
|
+
// in-memory map below is enough to develop against locally but DOES NOT
|
|
30
|
+
// satisfy NDPA Section 44 (record-keeping). Replace `lawfulBasisStore`/
|
|
31
|
+
// `auditLog` with your DB / KV / API.
|
|
32
|
+
interface LawfulBasisRecord {
|
|
33
|
+
id: string;
|
|
34
|
+
activityName: string;
|
|
35
|
+
lawfulBasis: string;
|
|
36
|
+
justification: string;
|
|
37
|
+
dataCategories: string[];
|
|
38
|
+
purposes: string[];
|
|
39
|
+
assessedBy: string;
|
|
40
|
+
reviewDate: Date | null;
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
updatedAt: Date;
|
|
43
|
+
}
|
|
44
|
+
const lawfulBasisStore = new Map<string, LawfulBasisRecord>();
|
|
45
|
+
const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
|
|
46
|
+
function newId() { return crypto.randomUUID(); }
|
|
47
|
+
// {{/if}}
|
|
48
|
+
|
|
49
|
+
// GET /api/lawful-basis?lawfulBasis=consent OR /api/lawful-basis?id=xxx
|
|
50
|
+
export async function GET(req: NextRequest) {
|
|
51
|
+
const id = req.nextUrl.searchParams.get('id');
|
|
52
|
+
|
|
53
|
+
if (id) {
|
|
54
|
+
// {{#if ORM=prisma}}
|
|
55
|
+
const record = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
|
|
56
|
+
// {{/if}}
|
|
57
|
+
// {{#if ORM=drizzle}}
|
|
58
|
+
const [record] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
|
|
59
|
+
// {{/if}}
|
|
60
|
+
// {{#if ORM=none}}
|
|
61
|
+
const record = lawfulBasisStore.get(id) ?? null;
|
|
62
|
+
// {{/if}}
|
|
63
|
+
|
|
64
|
+
if (!record) {
|
|
65
|
+
return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
|
|
66
|
+
}
|
|
67
|
+
return NextResponse.json(record);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lawfulBasis = req.nextUrl.searchParams.get('lawfulBasis');
|
|
71
|
+
|
|
72
|
+
// {{#if ORM=prisma}}
|
|
73
|
+
const records = await prisma.lawfulBasisRecord.findMany({
|
|
74
|
+
where: lawfulBasis ? { lawfulBasis } : undefined,
|
|
75
|
+
orderBy: { createdAt: 'desc' },
|
|
76
|
+
});
|
|
77
|
+
// {{/if}}
|
|
78
|
+
// {{#if ORM=drizzle}}
|
|
79
|
+
const records = lawfulBasis
|
|
80
|
+
? await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.lawfulBasis, lawfulBasis)).orderBy(desc(lawfulBasisRecords.createdAt))
|
|
81
|
+
: await db.select().from(lawfulBasisRecords).orderBy(desc(lawfulBasisRecords.createdAt));
|
|
82
|
+
// {{/if}}
|
|
83
|
+
// {{#if ORM=none}}
|
|
84
|
+
const records = [...lawfulBasisStore.values()]
|
|
85
|
+
.filter((r) => (lawfulBasis ? r.lawfulBasis === lawfulBasis : true))
|
|
86
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
87
|
+
// {{/if}}
|
|
88
|
+
|
|
89
|
+
return NextResponse.json(records);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// POST /api/lawful-basis
|
|
93
|
+
export async function POST(req: NextRequest) {
|
|
94
|
+
const body = await req.json();
|
|
95
|
+
const {
|
|
96
|
+
activityName,
|
|
97
|
+
lawfulBasis,
|
|
98
|
+
justification,
|
|
99
|
+
dataCategories,
|
|
100
|
+
purposes,
|
|
101
|
+
assessedBy,
|
|
102
|
+
reviewDate,
|
|
103
|
+
} = body;
|
|
104
|
+
|
|
105
|
+
if (!activityName || !lawfulBasis || !justification || !dataCategories || !purposes || !assessedBy) {
|
|
106
|
+
return NextResponse.json(
|
|
107
|
+
{ error: 'activityName, lawfulBasis, justification, dataCategories, purposes, and assessedBy are required' },
|
|
108
|
+
{ status: 400 },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!Array.isArray(dataCategories) || !Array.isArray(purposes)) {
|
|
113
|
+
return NextResponse.json(
|
|
114
|
+
{ error: 'dataCategories and purposes must be arrays' },
|
|
115
|
+
{ status: 400 },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// {{#if ORM=prisma}}
|
|
120
|
+
const record = await prisma.lawfulBasisRecord.create({
|
|
121
|
+
data: {
|
|
122
|
+
activityName,
|
|
123
|
+
lawfulBasis,
|
|
124
|
+
justification,
|
|
125
|
+
dataCategories,
|
|
126
|
+
purposes,
|
|
127
|
+
assessedBy,
|
|
128
|
+
reviewDate: reviewDate ? new Date(reviewDate) : null,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await prisma.complianceAuditLog.create({
|
|
133
|
+
data: {
|
|
134
|
+
module: 'lawful-basis',
|
|
135
|
+
action: 'created',
|
|
136
|
+
entityId: record.id,
|
|
137
|
+
entityType: 'LawfulBasisRecord',
|
|
138
|
+
changes: { activityName, lawfulBasis, assessedBy },
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
// {{/if}}
|
|
142
|
+
// {{#if ORM=drizzle}}
|
|
143
|
+
const [record] = await db.insert(lawfulBasisRecords).values({
|
|
144
|
+
activityName,
|
|
145
|
+
lawfulBasis,
|
|
146
|
+
justification,
|
|
147
|
+
dataCategories,
|
|
148
|
+
purposes,
|
|
149
|
+
assessedBy,
|
|
150
|
+
reviewDate: reviewDate ? new Date(reviewDate) : null,
|
|
151
|
+
}).returning();
|
|
152
|
+
|
|
153
|
+
await db.insert(complianceAuditLog).values({
|
|
154
|
+
module: 'lawful-basis',
|
|
155
|
+
action: 'created',
|
|
156
|
+
entityId: record.id,
|
|
157
|
+
entityType: 'LawfulBasisRecord',
|
|
158
|
+
changes: { activityName, lawfulBasis, assessedBy },
|
|
159
|
+
});
|
|
160
|
+
// {{/if}}
|
|
161
|
+
// {{#if ORM=none}}
|
|
162
|
+
const now = new Date();
|
|
163
|
+
const record: LawfulBasisRecord = {
|
|
164
|
+
id: newId(),
|
|
165
|
+
activityName,
|
|
166
|
+
lawfulBasis,
|
|
167
|
+
justification,
|
|
168
|
+
dataCategories,
|
|
169
|
+
purposes,
|
|
170
|
+
assessedBy,
|
|
171
|
+
reviewDate: reviewDate ? new Date(reviewDate) : null,
|
|
172
|
+
createdAt: now,
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
};
|
|
175
|
+
lawfulBasisStore.set(record.id, record);
|
|
176
|
+
auditLog.push({ id: newId(), module: 'lawful-basis', action: 'created', entityId: record.id, at: new Date() });
|
|
177
|
+
// {{/if}}
|
|
178
|
+
|
|
179
|
+
return NextResponse.json(record, { status: 201 });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// PUT /api/lawful-basis
|
|
183
|
+
export async function PUT(req: NextRequest) {
|
|
184
|
+
const body = await req.json();
|
|
185
|
+
const { id, ...data } = body;
|
|
186
|
+
|
|
187
|
+
if (!id) {
|
|
188
|
+
return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (data.reviewDate) {
|
|
192
|
+
data.reviewDate = new Date(data.reviewDate);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// {{#if ORM=prisma}}
|
|
196
|
+
const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
|
|
197
|
+
if (!existing) {
|
|
198
|
+
return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const record = await prisma.lawfulBasisRecord.update({
|
|
202
|
+
where: { id },
|
|
203
|
+
data,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await prisma.complianceAuditLog.create({
|
|
207
|
+
data: {
|
|
208
|
+
module: 'lawful-basis',
|
|
209
|
+
action: 'updated',
|
|
210
|
+
entityId: record.id,
|
|
211
|
+
entityType: 'LawfulBasisRecord',
|
|
212
|
+
changes: data,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
// {{/if}}
|
|
216
|
+
// {{#if ORM=drizzle}}
|
|
217
|
+
const [existing] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
|
|
218
|
+
if (!existing) {
|
|
219
|
+
return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const [record] = await db.update(lawfulBasisRecords).set(data).where(eq(lawfulBasisRecords.id, id)).returning();
|
|
223
|
+
|
|
224
|
+
await db.insert(complianceAuditLog).values({
|
|
225
|
+
module: 'lawful-basis',
|
|
226
|
+
action: 'updated',
|
|
227
|
+
entityId: record.id,
|
|
228
|
+
entityType: 'LawfulBasisRecord',
|
|
229
|
+
changes: data,
|
|
230
|
+
});
|
|
231
|
+
// {{/if}}
|
|
232
|
+
// {{#if ORM=none}}
|
|
233
|
+
const existing = lawfulBasisStore.get(id);
|
|
234
|
+
if (!existing) {
|
|
235
|
+
return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
|
|
236
|
+
}
|
|
237
|
+
const record: LawfulBasisRecord = { ...existing, ...data, id, updatedAt: new Date() };
|
|
238
|
+
lawfulBasisStore.set(id, record);
|
|
239
|
+
auditLog.push({ id: newId(), module: 'lawful-basis', action: 'updated', entityId: record.id, at: new Date() });
|
|
240
|
+
// {{/if}}
|
|
241
|
+
|
|
242
|
+
return NextResponse.json(record);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// DELETE /api/lawful-basis?id=xxx
|
|
246
|
+
export async function DELETE(req: NextRequest) {
|
|
247
|
+
const id = req.nextUrl.searchParams.get('id');
|
|
248
|
+
|
|
249
|
+
if (!id) {
|
|
250
|
+
return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// {{#if ORM=prisma}}
|
|
254
|
+
const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
|
|
255
|
+
if (!existing) {
|
|
256
|
+
return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await prisma.lawfulBasisRecord.delete({ where: { id } });
|
|
260
|
+
|
|
261
|
+
await prisma.complianceAuditLog.create({
|
|
262
|
+
data: {
|
|
263
|
+
module: 'lawful-basis',
|
|
264
|
+
action: 'deleted',
|
|
265
|
+
entityId: id,
|
|
266
|
+
entityType: 'LawfulBasisRecord',
|
|
267
|
+
changes: { activityName: existing.activityName },
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
// {{/if}}
|
|
271
|
+
// {{#if ORM=drizzle}}
|
|
272
|
+
const [existing] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
|
|
273
|
+
if (!existing) {
|
|
274
|
+
return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await db.delete(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id));
|
|
278
|
+
|
|
279
|
+
await db.insert(complianceAuditLog).values({
|
|
280
|
+
module: 'lawful-basis',
|
|
281
|
+
action: 'deleted',
|
|
282
|
+
entityId: id,
|
|
283
|
+
entityType: 'LawfulBasisRecord',
|
|
284
|
+
changes: { activityName: existing.activityName },
|
|
285
|
+
});
|
|
286
|
+
// {{/if}}
|
|
287
|
+
// {{#if ORM=none}}
|
|
288
|
+
const existing = lawfulBasisStore.get(id);
|
|
289
|
+
if (!existing) {
|
|
290
|
+
return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
|
|
291
|
+
}
|
|
292
|
+
lawfulBasisStore.delete(id);
|
|
293
|
+
auditLog.push({ id: newId(), module: 'lawful-basis', action: 'deleted', entityId: id, at: new Date() });
|
|
294
|
+
// {{/if}}
|
|
295
|
+
|
|
296
|
+
return NextResponse.json({ success: true });
|
|
297
|
+
}
|