@tantainnovative/create-ndpr 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 +130 -0
- package/bin/index.mjs +503 -0
- package/package.json +31 -0
- package/templates/drizzle-schema.ts +163 -0
- package/templates/env-example +4 -0
- package/templates/express-consent-route.ts +105 -0
- package/templates/express-setup.ts +250 -0
- package/templates/nextjs-breach-route.ts +107 -0
- package/templates/nextjs-consent-route.ts +100 -0
- package/templates/nextjs-dsr-route.ts +80 -0
- package/templates/nextjs-layout.tsx +64 -0
- package/templates/prisma-schema.prisma +117 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router — Consent API Route
|
|
3
|
+
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
|
+
*
|
|
5
|
+
* NDPA §25 (lawful basis) and §26 (right to withdraw consent).
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* GET /api/consent?subjectId=xxx — Load the active consent record
|
|
9
|
+
* POST /api/consent — Save new consent (revokes previous)
|
|
10
|
+
* DELETE /api/consent?subjectId=xxx — Revoke all active consent
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
14
|
+
import { PrismaClient } from '@prisma/client';
|
|
15
|
+
|
|
16
|
+
const prisma = new PrismaClient();
|
|
17
|
+
|
|
18
|
+
// GET /api/consent?subjectId=xxx
|
|
19
|
+
export async function GET(req: NextRequest) {
|
|
20
|
+
const subjectId = req.nextUrl.searchParams.get('subjectId');
|
|
21
|
+
|
|
22
|
+
if (!subjectId) {
|
|
23
|
+
return NextResponse.json({ error: 'subjectId required' }, { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const record = await prisma.consentRecord.findFirst({
|
|
27
|
+
where: { subjectId, revokedAt: null },
|
|
28
|
+
orderBy: { createdAt: 'desc' },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return NextResponse.json(record);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// POST /api/consent
|
|
35
|
+
export async function POST(req: NextRequest) {
|
|
36
|
+
const body = await req.json();
|
|
37
|
+
const { subjectId, consents, version, method, lawfulBasis } = body;
|
|
38
|
+
|
|
39
|
+
if (!subjectId || !consents || !version) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: 'subjectId, consents, and version are required' },
|
|
42
|
+
{ status: 400 },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Revoke any previously active consent records (immutable-audit pattern — NDPA §44).
|
|
47
|
+
await prisma.consentRecord.updateMany({
|
|
48
|
+
where: { subjectId, revokedAt: null },
|
|
49
|
+
data: { revokedAt: new Date() },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const record = await prisma.consentRecord.create({
|
|
53
|
+
data: {
|
|
54
|
+
subjectId,
|
|
55
|
+
consents,
|
|
56
|
+
version,
|
|
57
|
+
method: method ?? 'api',
|
|
58
|
+
lawfulBasis: lawfulBasis ?? null,
|
|
59
|
+
ipAddress: req.headers.get('x-forwarded-for'),
|
|
60
|
+
userAgent: req.headers.get('user-agent'),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await prisma.complianceAuditLog.create({
|
|
65
|
+
data: {
|
|
66
|
+
module: 'consent',
|
|
67
|
+
action: 'created',
|
|
68
|
+
entityId: record.id,
|
|
69
|
+
entityType: 'ConsentRecord',
|
|
70
|
+
changes: { subjectId, version, consents },
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return NextResponse.json(record, { status: 201 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// DELETE /api/consent?subjectId=xxx
|
|
78
|
+
export async function DELETE(req: NextRequest) {
|
|
79
|
+
const subjectId = req.nextUrl.searchParams.get('subjectId');
|
|
80
|
+
|
|
81
|
+
if (!subjectId) {
|
|
82
|
+
return NextResponse.json({ error: 'subjectId required' }, { status: 400 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await prisma.consentRecord.updateMany({
|
|
86
|
+
where: { subjectId, revokedAt: null },
|
|
87
|
+
data: { revokedAt: new Date() },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await prisma.complianceAuditLog.create({
|
|
91
|
+
data: {
|
|
92
|
+
module: 'consent',
|
|
93
|
+
action: 'revoked',
|
|
94
|
+
entityId: subjectId,
|
|
95
|
+
entityType: 'ConsentRecord',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return NextResponse.json({ success: true });
|
|
100
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router — DSR (Data Subject Rights) Route
|
|
3
|
+
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
|
+
*
|
|
5
|
+
* NDPA Part IV §34–38 — rights to access, rectification, erasure,
|
|
6
|
+
* portability, and objection. All requests must be acknowledged
|
|
7
|
+
* within 72 hours and fulfilled within 30 days.
|
|
8
|
+
*
|
|
9
|
+
* Endpoints:
|
|
10
|
+
* GET /api/dsr — List DSR requests (optional ?status= filter)
|
|
11
|
+
* POST /api/dsr — Submit a new DSR request
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
15
|
+
import { PrismaClient } from '@prisma/client';
|
|
16
|
+
|
|
17
|
+
const prisma = new PrismaClient();
|
|
18
|
+
|
|
19
|
+
// GET /api/dsr?status=pending
|
|
20
|
+
export async function GET(req: NextRequest) {
|
|
21
|
+
const status = req.nextUrl.searchParams.get('status');
|
|
22
|
+
|
|
23
|
+
const requests = await prisma.dSRRequest.findMany({
|
|
24
|
+
where: status ? { status } : undefined,
|
|
25
|
+
orderBy: { submittedAt: 'desc' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return NextResponse.json(requests);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// POST /api/dsr
|
|
32
|
+
export async function POST(req: NextRequest) {
|
|
33
|
+
const body = await req.json();
|
|
34
|
+
const {
|
|
35
|
+
type,
|
|
36
|
+
subjectName,
|
|
37
|
+
subjectEmail,
|
|
38
|
+
subjectPhone,
|
|
39
|
+
identifierType,
|
|
40
|
+
identifierValue,
|
|
41
|
+
description,
|
|
42
|
+
} = body;
|
|
43
|
+
|
|
44
|
+
if (!type || !subjectName || !subjectEmail || !identifierType || !identifierValue) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: 'type, subjectName, subjectEmail, identifierType, and identifierValue are required' },
|
|
47
|
+
{ status: 400 },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// NDPA mandates a 30-day response window from date of submission.
|
|
52
|
+
const dueAt = new Date();
|
|
53
|
+
dueAt.setDate(dueAt.getDate() + 30);
|
|
54
|
+
|
|
55
|
+
const request = await prisma.dSRRequest.create({
|
|
56
|
+
data: {
|
|
57
|
+
type,
|
|
58
|
+
subjectName,
|
|
59
|
+
subjectEmail,
|
|
60
|
+
subjectPhone: subjectPhone ?? null,
|
|
61
|
+
identifierType,
|
|
62
|
+
identifierValue,
|
|
63
|
+
description: description ?? null,
|
|
64
|
+
status: 'pending',
|
|
65
|
+
dueAt,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await prisma.complianceAuditLog.create({
|
|
70
|
+
data: {
|
|
71
|
+
module: 'dsr',
|
|
72
|
+
action: 'submitted',
|
|
73
|
+
entityId: request.id,
|
|
74
|
+
entityType: 'DSRRequest',
|
|
75
|
+
changes: { type, subjectEmail, status: 'pending' },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return NextResponse.json(request, { status: 201 });
|
|
80
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NDPRLayout — generated by create-ndpr
|
|
5
|
+
*
|
|
6
|
+
* Wraps your app with the NDPA-compliant consent banner and provider.
|
|
7
|
+
*
|
|
8
|
+
* Usage in app/layout.tsx:
|
|
9
|
+
*
|
|
10
|
+
* import NDPRLayout from '@/app/ndpr-layout';
|
|
11
|
+
*
|
|
12
|
+
* export default async function RootLayout({ children }) {
|
|
13
|
+
* const session = await getServerSession();
|
|
14
|
+
* return (
|
|
15
|
+
* <html lang="en">
|
|
16
|
+
* <body>
|
|
17
|
+
* <NDPRLayout userId={session?.user?.id}>
|
|
18
|
+
* {children}
|
|
19
|
+
* </NDPRLayout>
|
|
20
|
+
* </body>
|
|
21
|
+
* </html>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import { NDPRProvider } from '@tantainnovative/ndpr-toolkit/core';
|
|
28
|
+
import { NDPRConsent } from '@tantainnovative/ndpr-toolkit/presets';
|
|
29
|
+
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
|
|
30
|
+
|
|
31
|
+
interface NDPRLayoutProps {
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* Authenticated user ID, or a stable anonymous identifier (e.g. UUID from
|
|
35
|
+
* a cookie) for unauthenticated visitors. NDPA requires consent to be
|
|
36
|
+
* recorded even for anonymous users.
|
|
37
|
+
*/
|
|
38
|
+
userId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function NDPRLayout({ children, userId }: NDPRLayoutProps) {
|
|
42
|
+
const subjectId = userId ?? 'anonymous';
|
|
43
|
+
|
|
44
|
+
// apiAdapter connects the consent banner to the /api/consent route.
|
|
45
|
+
// Swap for prismaConsentAdapter or drizzleConsentAdapter for direct
|
|
46
|
+
// server-side usage in Server Components or Route Handlers.
|
|
47
|
+
const consentStorageAdapter = apiAdapter(`/api/consent?subjectId=${subjectId}`);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<NDPRProvider
|
|
51
|
+
organizationName="{{ORG_NAME}}"
|
|
52
|
+
dpoEmail="{{DPO_EMAIL}}"
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
|
|
56
|
+
{/*
|
|
57
|
+
* NDPA-compliant consent banner.
|
|
58
|
+
* The adapter persists decisions server-side so the banner is hidden
|
|
59
|
+
* on return visits without flicker or double-prompting.
|
|
60
|
+
*/}
|
|
61
|
+
<NDPRConsent adapter={consentStorageAdapter} />
|
|
62
|
+
</NDPRProvider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// NDPA Compliance Schema — generated by create-ndpr
|
|
2
|
+
// Organisation: {{ORG_NAME}}
|
|
3
|
+
// DPO: {{DPO_EMAIL}}
|
|
4
|
+
//
|
|
5
|
+
// Covers:
|
|
6
|
+
// - ConsentRecord — NDPA §25–26 (consent & withdrawal)
|
|
7
|
+
// - DSRRequest — NDPA Part IV §34–38 (data subject rights)
|
|
8
|
+
// - BreachReport — NDPA §40 (72-hour breach notification)
|
|
9
|
+
// - ProcessingRecord — ROPA (accountability principle)
|
|
10
|
+
// - ComplianceAuditLog — §44 audit trail
|
|
11
|
+
//
|
|
12
|
+
// Run `prisma migrate dev --name ndpr-init` to apply.
|
|
13
|
+
|
|
14
|
+
generator client {
|
|
15
|
+
provider = "prisma-client-js"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
datasource db {
|
|
19
|
+
provider = "postgresql"
|
|
20
|
+
url = env("DATABASE_URL")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
model ConsentRecord {
|
|
24
|
+
id String @id @default(cuid())
|
|
25
|
+
subjectId String
|
|
26
|
+
consents Json
|
|
27
|
+
version String
|
|
28
|
+
method String
|
|
29
|
+
lawfulBasis String?
|
|
30
|
+
ipAddress String?
|
|
31
|
+
userAgent String?
|
|
32
|
+
createdAt DateTime @default(now())
|
|
33
|
+
revokedAt DateTime?
|
|
34
|
+
|
|
35
|
+
@@index([subjectId])
|
|
36
|
+
@@map("ndpr_consent_records")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
model DSRRequest {
|
|
40
|
+
id String @id @default(cuid())
|
|
41
|
+
type String
|
|
42
|
+
status String @default("pending")
|
|
43
|
+
subjectName String
|
|
44
|
+
subjectEmail String
|
|
45
|
+
subjectPhone String?
|
|
46
|
+
identifierType String
|
|
47
|
+
identifierValue String
|
|
48
|
+
description String?
|
|
49
|
+
internalNotes String?
|
|
50
|
+
assignedTo String?
|
|
51
|
+
submittedAt DateTime @default(now())
|
|
52
|
+
acknowledgedAt DateTime?
|
|
53
|
+
completedAt DateTime?
|
|
54
|
+
dueAt DateTime
|
|
55
|
+
|
|
56
|
+
@@index([status])
|
|
57
|
+
@@index([subjectEmail])
|
|
58
|
+
@@map("ndpr_dsr_requests")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
model BreachReport {
|
|
62
|
+
id String @id @default(cuid())
|
|
63
|
+
title String
|
|
64
|
+
description String
|
|
65
|
+
category String
|
|
66
|
+
severity String
|
|
67
|
+
status String @default("ongoing")
|
|
68
|
+
discoveredAt DateTime
|
|
69
|
+
occurredAt DateTime?
|
|
70
|
+
reportedAt DateTime @default(now())
|
|
71
|
+
ndpcNotifiedAt DateTime?
|
|
72
|
+
reporterName String
|
|
73
|
+
reporterEmail String
|
|
74
|
+
reporterDepartment String?
|
|
75
|
+
affectedSystems Json
|
|
76
|
+
dataTypes Json
|
|
77
|
+
estimatedAffected Int?
|
|
78
|
+
initialActions String?
|
|
79
|
+
ndpcNotificationSent Boolean @default(false)
|
|
80
|
+
|
|
81
|
+
@@index([status])
|
|
82
|
+
@@index([severity])
|
|
83
|
+
@@map("ndpr_breach_reports")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
model ProcessingRecord {
|
|
87
|
+
id String @id @default(cuid())
|
|
88
|
+
purpose String
|
|
89
|
+
lawfulBasis String
|
|
90
|
+
dataCategories Json
|
|
91
|
+
dataSubjects Json
|
|
92
|
+
recipients Json
|
|
93
|
+
retentionPeriod String
|
|
94
|
+
securityMeasures Json
|
|
95
|
+
transferCountries Json?
|
|
96
|
+
transferMechanism String?
|
|
97
|
+
dpiaConducted Boolean @default(false)
|
|
98
|
+
status String @default("active")
|
|
99
|
+
createdAt DateTime @default(now())
|
|
100
|
+
updatedAt DateTime @updatedAt
|
|
101
|
+
|
|
102
|
+
@@map("ndpr_processing_records")
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
model ComplianceAuditLog {
|
|
106
|
+
id String @id @default(cuid())
|
|
107
|
+
module String
|
|
108
|
+
action String
|
|
109
|
+
entityId String
|
|
110
|
+
entityType String
|
|
111
|
+
changes Json?
|
|
112
|
+
performedBy String?
|
|
113
|
+
createdAt DateTime @default(now())
|
|
114
|
+
|
|
115
|
+
@@index([module, entityId])
|
|
116
|
+
@@map("ndpr_audit_log")
|
|
117
|
+
}
|