@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,161 +1,115 @@
1
1
  /**
2
- * Next.js App Router — DSR (Data Subject Rights) Route
3
- * Generated by create-ndpr for: {{ORG_NAME}}
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 '@/drizzle';
22
- import { dsrRequests, complianceAuditLog } from '@/drizzle/ndpr-schema';
23
- import { eq, desc } from 'drizzle-orm';
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
- // 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;
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, DSRRequest>();
44
- const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
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
- // GET /api/dsr?status=pending
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 } : undefined,
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((r) => (status ? r.status === status : true))
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 body = await req.json();
75
- const {
76
- type,
77
- subjectName,
78
- subjectEmail,
79
- subjectPhone,
80
- identifierType,
81
- identifierValue,
82
- description,
83
- } = body;
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
- // NDPA mandates a 30-day response window from date of submission.
93
- const dueAt = new Date();
94
- dueAt.setDate(dueAt.getDate() + 30);
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.dSRRequest.create({
98
- data: {
99
- type,
100
- subjectName,
101
- subjectEmail,
102
- subjectPhone: subjectPhone ?? null,
103
- identifierType,
104
- identifierValue,
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 [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' },
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: 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
- };
110
+ const request: DSRRow = { id: crypto.randomUUID(), ...data, submittedAt };
156
111
  dsrStore.set(request.id, request);
157
- auditLog.push({ id: newId(), module: 'dsr', action: 'submitted', entityId: request.id, at: new Date() });
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
  }