@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.
- package/README.md +64 -68
- package/bin/index.mjs +366 -526
- package/package.json +1 -1
- package/templates/drizzle-client.ts +44 -0
- package/templates/drizzle-schema.ts +200 -178
- package/templates/env-example +6 -3
- package/templates/express-breach-route.ts +98 -0
- package/templates/express-consent-route.ts +304 -153
- package/templates/express-cross-border-route.ts +94 -259
- package/templates/express-dpia-route.ts +267 -222
- package/templates/express-dsr-route.ts +76 -0
- package/templates/express-lawful-basis-route.ts +88 -234
- package/templates/express-request-context.ts +130 -0
- package/templates/github-ndpr-audit.yml +3 -3
- package/templates/nextjs-breach-route.ts +117 -183
- package/templates/nextjs-consent-route.ts +312 -104
- package/templates/nextjs-cross-border-route.ts +239 -250
- package/templates/nextjs-dpia-route.ts +434 -214
- package/templates/nextjs-dsr-route.ts +70 -116
- package/templates/nextjs-lawful-basis-route.ts +207 -221
- package/templates/nextjs-layout.tsx +120 -57
- package/templates/nextjs-request-context.ts +137 -0
- package/templates/prisma-schema.prisma +89 -77
- package/templates/express-setup.ts +0 -509
|
@@ -1,161 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Next.js App Router —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
9
|
-
* Endpoints:
|
|
10
|
-
* GET /api/dsr — List DSR requests (optional ?status= filter)
|
|
11
|
-
* POST /api/dsr — Submit a new DSR request
|
|
2
|
+
* Next.js App Router — tenant-scoped data-subject request route for {{ORG_NAME_COMMENT}}.
|
|
3
|
+
* Submission requires a verified subject; listing requires verified staff.
|
|
12
4
|
*/
|
|
13
|
-
|
|
14
5
|
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
import { getNDPRContextProblem, resolveNDPRRequestContext } from '{{NDPR_CONTEXT_IMPORT}}';
|
|
15
7
|
// {{#if ORM=prisma}}
|
|
16
8
|
import { PrismaClient } from '@prisma/client';
|
|
17
|
-
|
|
18
9
|
const prisma = new PrismaClient();
|
|
19
10
|
// {{/if}}
|
|
20
11
|
// {{#if ORM=drizzle}}
|
|
21
|
-
import { db } from '
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
12
|
+
import { db } from '{{NDPR_DB_IMPORT}}';
|
|
13
|
+
import { complianceAuditLog, dsrRequests } from '{{NDPR_SCHEMA_IMPORT}}';
|
|
14
|
+
import { and, desc, eq } from 'drizzle-orm';
|
|
24
15
|
// {{/if}}
|
|
25
16
|
// {{#if ORM=none}}
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
|
17
|
+
// DEVELOPMENT ONLY: replace both stores with one durable transactional store.
|
|
18
|
+
interface DSRRow {
|
|
19
|
+
id: string; tenantId: string; subjectId: string; type: string; status: string;
|
|
20
|
+
subjectName: string; subjectEmail: string; subjectPhone: string | null;
|
|
21
|
+
identifierType: string; identifierValue: string; description: string | null;
|
|
22
|
+
submittedAt: Date; dueAt: Date;
|
|
42
23
|
}
|
|
43
|
-
const dsrStore = new Map<string,
|
|
44
|
-
const auditLog: Array<{ id: string;
|
|
45
|
-
function newId() { return crypto.randomUUID(); }
|
|
24
|
+
const dsrStore = new Map<string, DSRRow>();
|
|
25
|
+
const auditLog: Array<{ id: string; tenantId: string; action: string; entityId: string; performedBy: string | null; at: Date }> = [];
|
|
46
26
|
// {{/if}}
|
|
47
27
|
|
|
48
|
-
|
|
28
|
+
const REQUEST_TYPES = new Set(['access', 'rectification', 'erasure', 'portability', 'objection', 'restriction']);
|
|
29
|
+
|
|
49
30
|
export async function GET(req: NextRequest) {
|
|
31
|
+
const context = await resolveNDPRRequestContext(req);
|
|
32
|
+
const problem = getNDPRContextProblem(context, 'staff');
|
|
33
|
+
if (problem) return NextResponse.json({ error: problem.error }, { status: problem.status });
|
|
50
34
|
const status = req.nextUrl.searchParams.get('status');
|
|
51
35
|
|
|
52
36
|
// {{#if ORM=prisma}}
|
|
53
37
|
const requests = await prisma.dSRRequest.findMany({
|
|
54
|
-
where: status ? { status } :
|
|
38
|
+
where: { tenantId: context.tenantId, ...(status ? { status } : {}) },
|
|
55
39
|
orderBy: { submittedAt: 'desc' },
|
|
56
40
|
});
|
|
57
41
|
// {{/if}}
|
|
58
42
|
// {{#if ORM=drizzle}}
|
|
59
43
|
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));
|
|
44
|
+
? await db.select().from(dsrRequests).where(and(eq(dsrRequests.tenantId, context.tenantId), eq(dsrRequests.status, status))).orderBy(desc(dsrRequests.submittedAt))
|
|
45
|
+
: await db.select().from(dsrRequests).where(eq(dsrRequests.tenantId, context.tenantId)).orderBy(desc(dsrRequests.submittedAt));
|
|
62
46
|
// {{/if}}
|
|
63
47
|
// {{#if ORM=none}}
|
|
64
48
|
const requests = [...dsrStore.values()]
|
|
65
|
-
.filter((
|
|
49
|
+
.filter((row) => row.tenantId === context.tenantId && (!status || row.status === status))
|
|
66
50
|
.sort((a, b) => b.submittedAt.getTime() - a.submittedAt.getTime());
|
|
67
51
|
// {{/if}}
|
|
68
|
-
|
|
69
52
|
return NextResponse.json(requests);
|
|
70
53
|
}
|
|
71
54
|
|
|
72
|
-
// POST /api/dsr
|
|
73
55
|
export async function POST(req: NextRequest) {
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!type || !subjectName || !subjectEmail || !identifierType || !identifierValue) {
|
|
86
|
-
return NextResponse.json(
|
|
87
|
-
{ error: 'type, subjectName, subjectEmail, identifierType, and identifierValue are required' },
|
|
88
|
-
{ status: 400 },
|
|
89
|
-
);
|
|
56
|
+
const context = await resolveNDPRRequestContext(req);
|
|
57
|
+
const problem = getNDPRContextProblem(context, 'subject');
|
|
58
|
+
if (problem) return NextResponse.json({ error: problem.error }, { status: problem.status });
|
|
59
|
+
const subjectId = context.subjectId as string;
|
|
60
|
+
const body: unknown = await req.json().catch(() => null);
|
|
61
|
+
if (!body || typeof body !== 'object') return NextResponse.json({ error: 'A JSON object is required' }, { status: 400 });
|
|
62
|
+
const input = body as Record<string, unknown>;
|
|
63
|
+
const required = ['type', 'subjectName', 'subjectEmail', 'identifierType', 'identifierValue'] as const;
|
|
64
|
+
if (required.some((field) => typeof input[field] !== 'string' || !(input[field] as string).trim())) {
|
|
65
|
+
return NextResponse.json({ error: `${required.join(', ')} are required strings` }, { status: 400 });
|
|
90
66
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
67
|
+
if (!REQUEST_TYPES.has(input.type as string)) {
|
|
68
|
+
return NextResponse.json({ error: `type must be one of: ${[...REQUEST_TYPES].join(', ')}` }, { status: 400 });
|
|
69
|
+
}
|
|
70
|
+
const submittedAt = new Date();
|
|
71
|
+
// Operational target; confirm and configure this deadline for your applicable obligations.
|
|
72
|
+
const dueAt = new Date(submittedAt.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
73
|
+
const data = {
|
|
74
|
+
tenantId: context.tenantId,
|
|
75
|
+
subjectId,
|
|
76
|
+
type: input.type as string,
|
|
77
|
+
subjectName: input.subjectName as string,
|
|
78
|
+
subjectEmail: input.subjectEmail as string,
|
|
79
|
+
subjectPhone: typeof input.subjectPhone === 'string' ? input.subjectPhone : null,
|
|
80
|
+
identifierType: input.identifierType as string,
|
|
81
|
+
identifierValue: input.identifierValue as string,
|
|
82
|
+
description: typeof input.description === 'string' ? input.description : null,
|
|
83
|
+
status: 'pending',
|
|
84
|
+
dueAt,
|
|
85
|
+
};
|
|
95
86
|
|
|
96
87
|
// {{#if ORM=prisma}}
|
|
97
|
-
const request = await prisma
|
|
98
|
-
data
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
description: description ?? null,
|
|
106
|
-
status: 'pending',
|
|
107
|
-
dueAt,
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
await prisma.complianceAuditLog.create({
|
|
112
|
-
data: {
|
|
113
|
-
module: 'dsr',
|
|
114
|
-
action: 'submitted',
|
|
115
|
-
entityId: request.id,
|
|
116
|
-
entityType: 'DSRRequest',
|
|
117
|
-
changes: { type, subjectEmail, status: 'pending' },
|
|
118
|
-
},
|
|
88
|
+
const request = await prisma.$transaction(async (tx) => {
|
|
89
|
+
const created = await tx.dSRRequest.create({ data });
|
|
90
|
+
await tx.complianceAuditLog.create({ data: {
|
|
91
|
+
tenantId: context.tenantId, module: 'dsr', action: 'submitted',
|
|
92
|
+
entityId: created.id, entityType: 'DSRRequest', performedBy: context.actorId,
|
|
93
|
+
changes: { type: data.type, subjectId, status: 'pending' },
|
|
94
|
+
} });
|
|
95
|
+
return created;
|
|
119
96
|
});
|
|
120
97
|
// {{/if}}
|
|
121
98
|
// {{#if ORM=drizzle}}
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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' },
|
|
99
|
+
const request = await db.transaction(async (tx) => {
|
|
100
|
+
const [created] = await tx.insert(dsrRequests).values(data).returning();
|
|
101
|
+
await tx.insert(complianceAuditLog).values({
|
|
102
|
+
tenantId: context.tenantId, module: 'dsr', action: 'submitted',
|
|
103
|
+
entityId: created.id, entityType: 'DSRRequest', performedBy: context.actorId,
|
|
104
|
+
changes: { type: data.type, subjectId, status: 'pending' },
|
|
105
|
+
});
|
|
106
|
+
return created;
|
|
140
107
|
});
|
|
141
108
|
// {{/if}}
|
|
142
109
|
// {{#if ORM=none}}
|
|
143
|
-
const request:
|
|
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
|
-
};
|
|
110
|
+
const request: DSRRow = { id: crypto.randomUUID(), ...data, submittedAt };
|
|
156
111
|
dsrStore.set(request.id, request);
|
|
157
|
-
auditLog.push({ id:
|
|
112
|
+
auditLog.push({ id: crypto.randomUUID(), tenantId: context.tenantId, action: 'submitted', entityId: request.id, performedBy: context.actorId, at: submittedAt });
|
|
158
113
|
// {{/if}}
|
|
159
|
-
|
|
160
114
|
return NextResponse.json(request, { status: 201 });
|
|
161
115
|
}
|