@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,73 +1,136 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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';
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { NDPRProvider } from '@tantainnovative/ndpr-toolkit';
|
|
28
5
|
import { NDPRConsent } from '@tantainnovative/ndpr-toolkit/presets';
|
|
29
6
|
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
|
|
30
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
StorageAdapter,
|
|
9
|
+
ConsentSettings,
|
|
10
|
+
} from '@tantainnovative/ndpr-toolkit/core';
|
|
31
11
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
12
|
+
const SUBJECT_STORAGE_KEY = 'ndpr_anonymous_subject_id';
|
|
13
|
+
const SUBJECT_COOKIE = 'ndpr_subject_id';
|
|
14
|
+
const ANONYMOUS_SUBJECT_PATTERN =
|
|
15
|
+
/^anon_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
16
|
+
|
|
17
|
+
interface StoredConsent {
|
|
18
|
+
consents: Record<string, boolean>;
|
|
19
|
+
version: string;
|
|
20
|
+
method: string;
|
|
21
|
+
lawfulBasis?: ConsentSettings['lawfulBasis'] | null;
|
|
22
|
+
hasInteracted?: boolean;
|
|
23
|
+
createdAt: string;
|
|
40
24
|
}
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
function getOrCreateAnonymousSubjectId(): string {
|
|
27
|
+
try {
|
|
28
|
+
const stored = window.localStorage.getItem(SUBJECT_STORAGE_KEY);
|
|
29
|
+
if (stored && ANONYMOUS_SUBJECT_PATTERN.test(stored)) return stored;
|
|
30
|
+
} catch {
|
|
31
|
+
// Storage can be disabled; the first-party cookie remains a fallback.
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cookie = document.cookie
|
|
35
|
+
.split(';')
|
|
36
|
+
.map((part) => part.trim())
|
|
37
|
+
.find((part) => part.startsWith(`${SUBJECT_COOKIE}=`));
|
|
38
|
+
if (cookie) {
|
|
39
|
+
try {
|
|
40
|
+
const existing = decodeURIComponent(cookie.slice(cookie.indexOf('=') + 1));
|
|
41
|
+
if (ANONYMOUS_SUBJECT_PATTERN.test(existing)) return existing;
|
|
42
|
+
} catch {
|
|
43
|
+
// Ignore malformed percent encoding and replace the invalid cookie below.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const subjectId = `anon_${crypto.randomUUID()}`;
|
|
48
|
+
try {
|
|
49
|
+
window.localStorage.setItem(SUBJECT_STORAGE_KEY, subjectId);
|
|
50
|
+
} catch {
|
|
51
|
+
// Cookie persistence below may still succeed.
|
|
52
|
+
}
|
|
53
|
+
document.cookie = [
|
|
54
|
+
`${SUBJECT_COOKIE}=${encodeURIComponent(subjectId)}`,
|
|
55
|
+
'path=/',
|
|
56
|
+
'max-age=31536000',
|
|
57
|
+
'samesite=lax',
|
|
58
|
+
location.protocol === 'https:' ? 'secure' : '',
|
|
59
|
+
].filter(Boolean).join('; ');
|
|
60
|
+
return subjectId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createConsentAdapter(
|
|
64
|
+
subjectId: string,
|
|
65
|
+
): StorageAdapter<ConsentSettings> {
|
|
66
|
+
const remote = apiAdapter<StoredConsent | ConsentSettings>('/api/consent', {
|
|
67
|
+
credentials: 'same-origin',
|
|
68
|
+
headers: { 'X-NDPR-Subject-Id': subjectId },
|
|
69
|
+
idempotencyKey: ({ method, payload }) => {
|
|
70
|
+
const timestamp = payload && 'timestamp' in payload
|
|
71
|
+
? payload.timestamp
|
|
72
|
+
: crypto.randomUUID();
|
|
73
|
+
return `${subjectId}:${method}:${timestamp}`;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
44
76
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
return {
|
|
78
|
+
capabilities: remote.capabilities,
|
|
79
|
+
async load() {
|
|
80
|
+
const record = await remote.load();
|
|
81
|
+
if (!record) return null;
|
|
82
|
+
if (
|
|
83
|
+
'timestamp' in record
|
|
84
|
+
&& typeof record.timestamp === 'number'
|
|
85
|
+
&& Number.isSafeInteger(record.timestamp)
|
|
86
|
+
) {
|
|
87
|
+
return record;
|
|
88
|
+
}
|
|
89
|
+
if (!('createdAt' in record)) return null;
|
|
90
|
+
const timestamp = Date.parse(record.createdAt);
|
|
91
|
+
if (!Number.isFinite(timestamp)) return null;
|
|
92
|
+
return {
|
|
93
|
+
consents: record.consents,
|
|
94
|
+
version: record.version,
|
|
95
|
+
method: record.method,
|
|
96
|
+
lawfulBasis: record.lawfulBasis ?? undefined,
|
|
97
|
+
timestamp,
|
|
98
|
+
hasInteracted: record.hasInteracted !== false,
|
|
99
|
+
};
|
|
55
100
|
},
|
|
101
|
+
save(settings) {
|
|
102
|
+
return remote.save(settings);
|
|
103
|
+
},
|
|
104
|
+
remove() {
|
|
105
|
+
return remote.remove();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface NDPRClientProviderProps { children: React.ReactNode }
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Client boundary for {{ORG_NAME_COMMENT}}. Authenticated identity is resolved by
|
|
114
|
+
* the server-side request-context hook; never pass account IDs from the
|
|
115
|
+
* browser as trusted identity.
|
|
116
|
+
*/
|
|
117
|
+
export default function NDPRClientProvider({ children }: NDPRClientProviderProps) {
|
|
118
|
+
const [anonymousSubjectId, setAnonymousSubjectId] = useState<string | null>(null);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setAnonymousSubjectId(getOrCreateAnonymousSubjectId());
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const consentAdapter = useMemo(
|
|
124
|
+
() => anonymousSubjectId
|
|
125
|
+
? createConsentAdapter(anonymousSubjectId)
|
|
126
|
+
: null,
|
|
127
|
+
[anonymousSubjectId],
|
|
56
128
|
);
|
|
57
129
|
|
|
58
130
|
return (
|
|
59
|
-
<NDPRProvider
|
|
60
|
-
organizationName="{{ORG_NAME}}"
|
|
61
|
-
dpoEmail="{{DPO_EMAIL}}"
|
|
62
|
-
>
|
|
131
|
+
<NDPRProvider organizationName={`{{ORG_NAME_TEMPLATE}}`} dpoEmail={`{{DPO_EMAIL_TEMPLATE}}`}>
|
|
63
132
|
{children}
|
|
64
|
-
|
|
65
|
-
{/*
|
|
66
|
-
* NDPA-compliant consent banner.
|
|
67
|
-
* The adapter persists decisions server-side so the banner is hidden
|
|
68
|
-
* on return visits without flicker or double-prompting.
|
|
69
|
-
*/}
|
|
70
|
-
<NDPRConsent adapter={consentStorageAdapter} />
|
|
133
|
+
{consentAdapter ? <NDPRConsent adapter={consentAdapter} /> : null}
|
|
71
134
|
</NDPRProvider>
|
|
72
135
|
);
|
|
73
136
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
export interface NDPRVerifiedActor {
|
|
4
|
+
/** Stable identity and profile fields from a verified server session. */
|
|
5
|
+
id: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
email: string;
|
|
8
|
+
department?: string;
|
|
9
|
+
/** Stable data-subject identifier for this account, when applicable. */
|
|
10
|
+
subjectId?: string;
|
|
11
|
+
/** Map application roles to ndpr:staff or ndpr:admin server-side. */
|
|
12
|
+
roles: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NDPRRequestContext {
|
|
16
|
+
/** Verified tenant boundary. Never take this value from request input. */
|
|
17
|
+
tenantId: string;
|
|
18
|
+
/** Normalized verified actor; null for public/anonymous requests. */
|
|
19
|
+
actor: NDPRVerifiedActor | null;
|
|
20
|
+
actorId: string | null;
|
|
21
|
+
/** Verified account subject ID or high-entropy anonymous browser ID. */
|
|
22
|
+
subjectId: string | null;
|
|
23
|
+
subjectSource: 'verified-account-subject' | 'anonymous-uuid-capability' | null;
|
|
24
|
+
/** Verified application roles used by your authorization policy. */
|
|
25
|
+
roles: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type NDPRContextRequirement = 'tenant' | 'subject' | 'staff';
|
|
29
|
+
export interface NDPRContextProblem { status: 401 | 403 | 503; error: string }
|
|
30
|
+
export type NDPRVerifiedActorResolver = (
|
|
31
|
+
request: NextRequest,
|
|
32
|
+
) => Promise<NDPRVerifiedActor | null>;
|
|
33
|
+
|
|
34
|
+
const ANONYMOUS_SUBJECT_PATTERN =
|
|
35
|
+
/^anon_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
36
|
+
const STAFF_ROLES = new Set(['ndpr:staff', 'ndpr:admin']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Authentication integration seam. Replace this safe default with your
|
|
40
|
+
* verified server session provider. Never trust actor, account subject,
|
|
41
|
+
* profile, role, or tenant fields from request-controlled input.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveVerifiedNDPRActor(
|
|
44
|
+
_request: NextRequest,
|
|
45
|
+
): Promise<NDPRVerifiedActor | null> {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The tenant comes only from server configuration. The only client-provided
|
|
51
|
+
* identity accepted by default is a random anon_<UUID> subject capability;
|
|
52
|
+
* it never grants staff access.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveNDPRRequestContext(
|
|
55
|
+
request: NextRequest,
|
|
56
|
+
actorResolver: NDPRVerifiedActorResolver = resolveVerifiedNDPRActor,
|
|
57
|
+
): Promise<NDPRRequestContext> {
|
|
58
|
+
const tenantId = process.env.NDPR_TENANT_ID?.trim() ?? '';
|
|
59
|
+
const candidate = request.headers.get('x-ndpr-subject-id')?.trim() ?? '';
|
|
60
|
+
const anonymousSubjectId = ANONYMOUS_SUBJECT_PATTERN.test(candidate)
|
|
61
|
+
? candidate
|
|
62
|
+
: null;
|
|
63
|
+
const actor = normalizeVerifiedActor(await actorResolver(request));
|
|
64
|
+
const subjectId = actor?.subjectId ?? anonymousSubjectId;
|
|
65
|
+
const subjectSource = actor?.subjectId
|
|
66
|
+
? 'verified-account-subject' as const
|
|
67
|
+
: anonymousSubjectId
|
|
68
|
+
? 'anonymous-uuid-capability' as const
|
|
69
|
+
: null;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
tenantId,
|
|
73
|
+
actor,
|
|
74
|
+
actorId: actor?.id ?? null,
|
|
75
|
+
subjectId,
|
|
76
|
+
subjectSource,
|
|
77
|
+
roles: actor?.roles ?? [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getNDPRContextProblem(
|
|
82
|
+
context: NDPRRequestContext,
|
|
83
|
+
requirement: NDPRContextRequirement,
|
|
84
|
+
): NDPRContextProblem | null {
|
|
85
|
+
if (!context.tenantId) {
|
|
86
|
+
return {
|
|
87
|
+
status: 503,
|
|
88
|
+
error: 'NDPR_TENANT_ID is not configured on the server',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (requirement === 'subject' && !context.subjectId) {
|
|
92
|
+
return { status: 401, error: 'A verified data-subject identity is required' };
|
|
93
|
+
}
|
|
94
|
+
if (requirement === 'staff' && !context.actorId) {
|
|
95
|
+
return {
|
|
96
|
+
status: 401,
|
|
97
|
+
error: 'Connect resolveVerifiedNDPRActor to verified staff authentication',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (requirement === 'staff'
|
|
101
|
+
&& !context.roles.some((role) => STAFF_ROLES.has(role))) {
|
|
102
|
+
return { status: 403, error: 'NDPR staff or administrator role required' };
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeVerifiedActor(
|
|
108
|
+
actor: NDPRVerifiedActor | null,
|
|
109
|
+
): NDPRVerifiedActor | null {
|
|
110
|
+
if (!actor
|
|
111
|
+
|| typeof actor.id !== 'string'
|
|
112
|
+
|| typeof actor.displayName !== 'string'
|
|
113
|
+
|| typeof actor.email !== 'string'
|
|
114
|
+
|| !Array.isArray(actor.roles)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const id = actor.id.trim();
|
|
118
|
+
const displayName = actor.displayName.trim();
|
|
119
|
+
const email = actor.email.trim();
|
|
120
|
+
if (!id || !displayName || !email) return null;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
displayName,
|
|
125
|
+
email,
|
|
126
|
+
department: typeof actor.department === 'string'
|
|
127
|
+
? actor.department.trim() || undefined
|
|
128
|
+
: undefined,
|
|
129
|
+
subjectId: typeof actor.subjectId === 'string'
|
|
130
|
+
? actor.subjectId.trim() || undefined
|
|
131
|
+
: undefined,
|
|
132
|
+
roles: actor.roles
|
|
133
|
+
.filter((role): role is string => typeof role === 'string')
|
|
134
|
+
.map((role) => role.trim())
|
|
135
|
+
.filter(Boolean),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
// NDPA
|
|
2
|
-
//
|
|
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
|
-
// - DPIARecord — NDPA §34(3) (impact assessments)
|
|
11
|
-
// - LawfulBasisRecord — NDPA §25 (lawful basis register)
|
|
12
|
-
// - CrossBorderTransferRecord — NDPA §41–42 (cross-border transfers)
|
|
13
|
-
// - ComplianceAuditLog — §44 audit trail
|
|
14
|
-
//
|
|
15
|
-
// Run `prisma migrate dev --name ndpr-init` to apply.
|
|
1
|
+
// NDPA compliance schema — generated by create-ndpr for {{ORG_NAME_COMMENT}}
|
|
2
|
+
// Every row is tenant-scoped. Derive tenantId only from verified server context.
|
|
16
3
|
|
|
17
4
|
generator client {
|
|
18
5
|
provider = "prisma-client-js"
|
|
@@ -24,25 +11,32 @@ datasource db {
|
|
|
24
11
|
}
|
|
25
12
|
|
|
26
13
|
model ConsentRecord {
|
|
27
|
-
id
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
14
|
+
id String @id @default(cuid())
|
|
15
|
+
tenantId String
|
|
16
|
+
subjectId String
|
|
17
|
+
activeSubjectKey String? @unique
|
|
18
|
+
consents Json
|
|
19
|
+
version String
|
|
20
|
+
method String
|
|
21
|
+
hasInteracted Boolean
|
|
22
|
+
lawfulBasis String?
|
|
23
|
+
ipAddress String?
|
|
24
|
+
userAgent String?
|
|
25
|
+
clientTimestamp DateTime
|
|
26
|
+
createdAt DateTime @default(now())
|
|
27
|
+
revokedAt DateTime?
|
|
28
|
+
|
|
29
|
+
@@index([tenantId, subjectId, revokedAt])
|
|
30
|
+
@@index([tenantId, subjectId, clientTimestamp, version, method])
|
|
39
31
|
@@map("ndpr_consent_records")
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
model DSRRequest {
|
|
43
|
-
id String
|
|
35
|
+
id String @id @default(cuid())
|
|
36
|
+
tenantId String
|
|
37
|
+
subjectId String
|
|
44
38
|
type String
|
|
45
|
-
status String
|
|
39
|
+
status String @default("pending")
|
|
46
40
|
subjectName String
|
|
47
41
|
subjectEmail String
|
|
48
42
|
subjectPhone String?
|
|
@@ -51,43 +45,53 @@ model DSRRequest {
|
|
|
51
45
|
description String?
|
|
52
46
|
internalNotes String?
|
|
53
47
|
assignedTo String?
|
|
54
|
-
submittedAt DateTime
|
|
48
|
+
submittedAt DateTime @default(now())
|
|
55
49
|
acknowledgedAt DateTime?
|
|
56
50
|
completedAt DateTime?
|
|
57
51
|
dueAt DateTime
|
|
58
52
|
|
|
59
|
-
@@index([status])
|
|
60
|
-
@@index([
|
|
53
|
+
@@index([tenantId, status])
|
|
54
|
+
@@index([tenantId, subjectId])
|
|
55
|
+
@@index([tenantId, subjectEmail])
|
|
61
56
|
@@map("ndpr_dsr_requests")
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
model BreachReport {
|
|
65
|
-
id
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
60
|
+
id String @id @default(cuid())
|
|
61
|
+
tenantId String
|
|
62
|
+
title String
|
|
63
|
+
description String
|
|
64
|
+
category String
|
|
65
|
+
severity String
|
|
66
|
+
status String @default("ongoing")
|
|
67
|
+
discoveredAt DateTime
|
|
68
|
+
occurredAt DateTime?
|
|
69
|
+
reportedAt DateTime @default(now())
|
|
70
|
+
ndpcNotifiedAt DateTime?
|
|
71
|
+
reporterName String
|
|
72
|
+
reporterEmail String
|
|
73
|
+
reporterDepartment String?
|
|
74
|
+
affectedSystems Json
|
|
75
|
+
dataTypes Json
|
|
76
|
+
involvesSensitiveData Boolean @default(false)
|
|
77
|
+
estimatedAffected Int?
|
|
78
|
+
approximateRecordCount Int?
|
|
79
|
+
dataSubjectCategories Json
|
|
80
|
+
likelyConsequences String?
|
|
81
|
+
mitigationMeasures String?
|
|
82
|
+
isPhasedReport Boolean @default(false)
|
|
83
|
+
supplementsReportId String?
|
|
84
|
+
initialActions String?
|
|
85
|
+
ndpcNotificationSent Boolean @default(false)
|
|
86
|
+
|
|
87
|
+
@@index([tenantId, status])
|
|
88
|
+
@@index([tenantId, severity])
|
|
86
89
|
@@map("ndpr_breach_reports")
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
model ProcessingRecord {
|
|
90
93
|
id String @id @default(cuid())
|
|
94
|
+
tenantId String
|
|
91
95
|
purpose String
|
|
92
96
|
lawfulBasis String
|
|
93
97
|
dataCategories Json
|
|
@@ -102,29 +106,32 @@ model ProcessingRecord {
|
|
|
102
106
|
createdAt DateTime @default(now())
|
|
103
107
|
updatedAt DateTime @updatedAt
|
|
104
108
|
|
|
109
|
+
@@index([tenantId, status])
|
|
105
110
|
@@map("ndpr_processing_records")
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
model DPIARecord {
|
|
109
|
-
id
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
id String @id @default(cuid())
|
|
115
|
+
tenantId String
|
|
116
|
+
projectName String
|
|
117
|
+
description String
|
|
118
|
+
dpiaData Json
|
|
119
|
+
overallRisk String
|
|
120
|
+
score Int
|
|
121
|
+
status String @default("draft")
|
|
122
|
+
conductedBy String
|
|
123
|
+
approvedBy String?
|
|
124
|
+
createdAt DateTime @default(now())
|
|
125
|
+
updatedAt DateTime @updatedAt
|
|
126
|
+
removedAt DateTime?
|
|
127
|
+
|
|
128
|
+
@@index([tenantId, removedAt, status])
|
|
123
129
|
@@map("ndpr_dpia_records")
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
model LawfulBasisRecord {
|
|
127
133
|
id String @id @default(cuid())
|
|
134
|
+
tenantId String
|
|
128
135
|
activityName String
|
|
129
136
|
lawfulBasis String
|
|
130
137
|
justification String
|
|
@@ -135,33 +142,38 @@ model LawfulBasisRecord {
|
|
|
135
142
|
reviewDate DateTime?
|
|
136
143
|
createdAt DateTime @default(now())
|
|
137
144
|
updatedAt DateTime @updatedAt
|
|
145
|
+
removedAt DateTime?
|
|
138
146
|
|
|
139
|
-
@@index([lawfulBasis])
|
|
140
|
-
@@index([assessedBy])
|
|
147
|
+
@@index([tenantId, removedAt, lawfulBasis])
|
|
141
148
|
@@map("ndpr_lawful_basis_records")
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
model CrossBorderTransferRecord {
|
|
145
|
-
id String
|
|
152
|
+
id String @id @default(cuid())
|
|
153
|
+
tenantId String
|
|
146
154
|
destinationCountry String
|
|
147
155
|
recipientName String
|
|
148
156
|
transferMechanism String
|
|
149
157
|
safeguards String
|
|
150
158
|
dataCategories Json
|
|
151
159
|
adequacyStatus String
|
|
152
|
-
ndpcApprovalRequired Boolean
|
|
160
|
+
ndpcApprovalRequired Boolean @default(false)
|
|
153
161
|
ndpcApprovalReference String?
|
|
154
162
|
riskLevel String
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
status String @default("active")
|
|
164
|
+
createdAt DateTime @default(now())
|
|
165
|
+
updatedAt DateTime @updatedAt
|
|
166
|
+
removedAt DateTime?
|
|
167
|
+
|
|
168
|
+
@@index([tenantId, removedAt, destinationCountry])
|
|
169
|
+
@@index([tenantId, removedAt, riskLevel])
|
|
170
|
+
@@index([tenantId, removedAt, status])
|
|
160
171
|
@@map("ndpr_cross_border_transfer_records")
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
model ComplianceAuditLog {
|
|
164
175
|
id String @id @default(cuid())
|
|
176
|
+
tenantId String
|
|
165
177
|
module String
|
|
166
178
|
action String
|
|
167
179
|
entityId String
|
|
@@ -170,6 +182,6 @@ model ComplianceAuditLog {
|
|
|
170
182
|
performedBy String?
|
|
171
183
|
createdAt DateTime @default(now())
|
|
172
184
|
|
|
173
|
-
@@index([module, entityId])
|
|
185
|
+
@@index([tenantId, module, entityId])
|
|
174
186
|
@@map("ndpr_audit_log")
|
|
175
187
|
}
|