@tantainnovative/ndpr-recipes 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 +408 -0
- package/package.json +25 -0
- package/prisma/schema.prisma +104 -0
- package/src/adapters/drizzle-consent.ts +135 -0
- package/src/adapters/drizzle-dsr.ts +190 -0
- package/src/adapters/index.ts +39 -0
- package/src/adapters/prisma-breach.ts +181 -0
- package/src/adapters/prisma-consent.ts +129 -0
- package/src/adapters/prisma-dsr.ts +163 -0
- package/src/adapters/prisma-ropa.ts +205 -0
- package/src/drizzle/schema.ts +484 -0
- package/src/express/index.ts +82 -0
- package/src/express/middleware/consent-check.ts +133 -0
- package/src/express/routes/breach.ts +259 -0
- package/src/express/routes/compliance.ts +130 -0
- package/src/express/routes/consent.ts +163 -0
- package/src/express/routes/dsr.ts +203 -0
- package/src/express/routes/ropa.ts +225 -0
- package/src/nextjs/app-router/api/breach/[id]/route.ts +121 -0
- package/src/nextjs/app-router/api/breach/route.ts +182 -0
- package/src/nextjs/app-router/api/compliance/route.ts +162 -0
- package/src/nextjs/app-router/api/consent/route.ts +161 -0
- package/src/nextjs/app-router/api/dsr/[id]/route.ts +115 -0
- package/src/nextjs/app-router/api/dsr/route.ts +128 -0
- package/src/nextjs/app-router/api/ropa/route.ts +234 -0
- package/src/nextjs/app-router/layout-example.tsx +178 -0
- package/src/nextjs/app-router/middleware.ts +113 -0
package/README.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# @tantainnovative/ndpr-recipes
|
|
2
|
+
|
|
3
|
+
Backend recipes for NDPA compliance with [@tantainnovative/ndpr-toolkit](https://github.com/tantainnovative/ndpr-toolkit).
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
This package is a **reference implementation** — not a library to install. Copy the files you need directly into your project and adapt them to fit your architecture. Each recipe is self-contained and heavily documented.
|
|
8
|
+
|
|
9
|
+
> Do not `npm install` this package into your project. Clone or download the files and integrate them manually.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`ndpr-recipes` provides everything you need to back the `@tantainnovative/ndpr-toolkit` with a real database. It covers two ORM families, two server frameworks, and includes complete examples for wiring it all together.
|
|
16
|
+
|
|
17
|
+
### What's covered
|
|
18
|
+
|
|
19
|
+
| Coverage area | Implementation |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Database schema | Prisma + Drizzle ORM (PostgreSQL) |
|
|
22
|
+
| Consent persistence | Prisma adapter, Drizzle adapter |
|
|
23
|
+
| DSR request persistence | Prisma adapter, Drizzle adapter |
|
|
24
|
+
| Breach report persistence | Prisma adapter |
|
|
25
|
+
| ROPA persistence | Prisma adapter |
|
|
26
|
+
| Next.js App Router | Consent, DSR, Breach, ROPA, Compliance route handlers |
|
|
27
|
+
| Express | Full NDPR router with consent, DSR, breach, ROPA, compliance routes |
|
|
28
|
+
| Consent middleware | Next.js edge middleware + Express middleware |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Available Recipes
|
|
33
|
+
|
|
34
|
+
| File | Description |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `prisma/schema.prisma` | Prisma schema — all 5 NDPA compliance tables |
|
|
37
|
+
| `src/drizzle/schema.ts` | Drizzle ORM schema — mirrors the Prisma schema |
|
|
38
|
+
| `src/adapters/prisma-consent.ts` | Prisma `StorageAdapter<ConsentSettings>` |
|
|
39
|
+
| `src/adapters/prisma-dsr.ts` | Prisma `StorageAdapter<DSRRequest[]>` |
|
|
40
|
+
| `src/adapters/prisma-breach.ts` | Prisma `StorageAdapter<BreachState>` |
|
|
41
|
+
| `src/adapters/prisma-ropa.ts` | Prisma `StorageAdapter<RecordOfProcessingActivities>` |
|
|
42
|
+
| `src/adapters/drizzle-consent.ts` | Drizzle `StorageAdapter<ConsentSettings>` |
|
|
43
|
+
| `src/adapters/drizzle-dsr.ts` | Drizzle `StorageAdapter<DSRRequest[]>` |
|
|
44
|
+
| `src/nextjs/app-router/api/consent/route.ts` | Next.js consent API route |
|
|
45
|
+
| `src/nextjs/app-router/api/dsr/route.ts` | Next.js DSR API route |
|
|
46
|
+
| `src/nextjs/app-router/api/breach/route.ts` | Next.js breach API route |
|
|
47
|
+
| `src/nextjs/app-router/api/ropa/route.ts` | Next.js ROPA API route |
|
|
48
|
+
| `src/nextjs/app-router/api/compliance/route.ts` | Next.js compliance score API route |
|
|
49
|
+
| `src/nextjs/app-router/middleware.ts` | Next.js consent gate middleware |
|
|
50
|
+
| `src/nextjs/app-router/layout-example.tsx` | Full wiring example for App Router |
|
|
51
|
+
| `src/express/index.ts` | Express router factory — mounts all routes |
|
|
52
|
+
| `src/express/routes/consent.ts` | Express consent router |
|
|
53
|
+
| `src/express/routes/dsr.ts` | Express DSR router |
|
|
54
|
+
| `src/express/routes/breach.ts` | Express breach router |
|
|
55
|
+
| `src/express/routes/ropa.ts` | Express ROPA router |
|
|
56
|
+
| `src/express/routes/compliance.ts` | Express compliance score router |
|
|
57
|
+
| `src/express/middleware/consent-check.ts` | Express consent gate middleware |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
### 1. Copy the database schema
|
|
64
|
+
|
|
65
|
+
**Prisma:**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Copy into your project
|
|
69
|
+
cp packages/ndpr-recipes/prisma/schema.prisma prisma/schema.prisma
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Drizzle:**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Copy the schema file
|
|
76
|
+
cp packages/ndpr-recipes/src/drizzle/schema.ts src/db/ndpr-schema.ts
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Set up the database connection
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# .env
|
|
83
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/myapp_dev"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. Run migrations
|
|
87
|
+
|
|
88
|
+
**Prisma:**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx prisma migrate dev --name init-ndpr-tables
|
|
92
|
+
npx prisma generate
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Drizzle:**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx drizzle-kit push
|
|
99
|
+
# or generate a migration file:
|
|
100
|
+
npx drizzle-kit generate
|
|
101
|
+
npx drizzle-kit migrate
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. Copy and wire the adapters
|
|
105
|
+
|
|
106
|
+
Pick the adapter for your ORM (see sections below), copy it into your project, and pass it to the relevant toolkit hook.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Prisma Adapters
|
|
111
|
+
|
|
112
|
+
The adapters in `src/adapters/prisma-*.ts` implement the `StorageAdapter<T>` interface from `@tantainnovative/ndpr-toolkit`. Copy them alongside your Prisma client and pass them to the corresponding toolkit hook.
|
|
113
|
+
|
|
114
|
+
### Consent adapter
|
|
115
|
+
|
|
116
|
+
Follows the immutable-audit pattern required by NDPA Section 25: records are never deleted, and revocation sets `revokedAt` on the existing row.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { PrismaClient } from '@prisma/client';
|
|
120
|
+
import { useConsent } from '@tantainnovative/ndpr-toolkit';
|
|
121
|
+
import { prismaConsentAdapter } from './adapters/prisma-consent';
|
|
122
|
+
|
|
123
|
+
const prisma = new PrismaClient();
|
|
124
|
+
|
|
125
|
+
function ConsentBanner() {
|
|
126
|
+
const adapter = prismaConsentAdapter(prisma, session.userId);
|
|
127
|
+
const { settings, updateConsent } = useConsent({ adapter });
|
|
128
|
+
// ...
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### DSR adapter
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { prismaDSRAdapter } from './adapters/prisma-dsr';
|
|
136
|
+
|
|
137
|
+
const adapter = prismaDSRAdapter(prisma, session.user.email);
|
|
138
|
+
// Pass to useDSR({ adapter }) or call adapter.save(requests) in a route handler
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Breach adapter
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { prismaBreachAdapter } from './adapters/prisma-breach';
|
|
145
|
+
|
|
146
|
+
const adapter = prismaBreachAdapter(prisma);
|
|
147
|
+
// Pass to useBreach({ adapter })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### ROPA adapter
|
|
151
|
+
|
|
152
|
+
Organisation metadata (name, DPO contact, address) is not stored in the database — supply it when constructing the adapter.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { prismaROPAAdapter } from './adapters/prisma-ropa';
|
|
156
|
+
|
|
157
|
+
const adapter = prismaROPAAdapter(prisma, {
|
|
158
|
+
organizationName: process.env.ORG_NAME!,
|
|
159
|
+
organizationContact: process.env.DPO_EMAIL!,
|
|
160
|
+
organizationAddress: process.env.ORG_ADDRESS!,
|
|
161
|
+
ndpcRegistrationNumber: process.env.NDPC_REG_NUMBER,
|
|
162
|
+
});
|
|
163
|
+
// Pass to useROPA({ adapter })
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Drizzle Adapters
|
|
169
|
+
|
|
170
|
+
The adapters in `src/adapters/drizzle-*.ts` use the same `StorageAdapter<T>` interface but target a Drizzle `db` instance instead of Prisma. The schema lives in `src/drizzle/schema.ts`.
|
|
171
|
+
|
|
172
|
+
### Setup
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pnpm add drizzle-orm pg @paralleldrive/cuid2
|
|
176
|
+
pnpm add -D drizzle-kit @types/pg
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// src/db.ts
|
|
181
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
182
|
+
import { Pool } from 'pg';
|
|
183
|
+
import * as schema from './drizzle/schema';
|
|
184
|
+
|
|
185
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
186
|
+
export const db = drizzle(pool, { schema });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Consent adapter
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { drizzleConsentAdapter } from './adapters/drizzle-consent';
|
|
193
|
+
|
|
194
|
+
const adapter = drizzleConsentAdapter(db, session.userId);
|
|
195
|
+
const { settings, updateConsent } = useConsent({ adapter });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### DSR adapter
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { drizzleDSRAdapter } from './adapters/drizzle-dsr';
|
|
202
|
+
|
|
203
|
+
const adapter = drizzleDSRAdapter(db, session.user.email);
|
|
204
|
+
const { requests, submitRequest } = useDSR({ adapter });
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Next.js Integration
|
|
210
|
+
|
|
211
|
+
### App Router route handlers
|
|
212
|
+
|
|
213
|
+
Copy the API routes from `src/nextjs/app-router/api/` into your project's `app/api/` directory:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Consent management
|
|
217
|
+
cp src/nextjs/app-router/api/consent/route.ts app/api/consent/route.ts
|
|
218
|
+
|
|
219
|
+
# Data subject rights
|
|
220
|
+
cp src/nextjs/app-router/api/dsr/route.ts app/api/dsr/route.ts
|
|
221
|
+
|
|
222
|
+
# Breach reports
|
|
223
|
+
cp src/nextjs/app-router/api/breach/route.ts app/api/breach/route.ts
|
|
224
|
+
|
|
225
|
+
# ROPA
|
|
226
|
+
cp src/nextjs/app-router/api/ropa/route.ts app/api/ropa/route.ts
|
|
227
|
+
|
|
228
|
+
# Compliance score
|
|
229
|
+
cp src/nextjs/app-router/api/compliance/route.ts app/api/compliance/route.ts
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Each route is fully documented with its HTTP methods, query params, and body shape at the top of the file.
|
|
233
|
+
|
|
234
|
+
### Consent middleware (route protection)
|
|
235
|
+
|
|
236
|
+
Protect any route that requires a specific consent type:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// app/api/email/marketing/route.ts
|
|
240
|
+
import { consentMiddleware } from '@/ndpr/middleware';
|
|
241
|
+
|
|
242
|
+
export async function POST(req: NextRequest) {
|
|
243
|
+
const guard = await consentMiddleware(req, 'marketing');
|
|
244
|
+
if (guard) return guard; // 403 if consent not granted
|
|
245
|
+
|
|
246
|
+
// Proceed — subject has consented to marketing
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Or use the higher-order wrapper:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { withConsent } from '@/ndpr/middleware';
|
|
254
|
+
|
|
255
|
+
export const POST = withConsent('marketing', async (req) => {
|
|
256
|
+
// marketing consent guaranteed here
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Full layout example
|
|
261
|
+
|
|
262
|
+
See `src/nextjs/app-router/layout-example.tsx` for a complete wiring example. Copy it to `components/ndpr-layout.tsx` and add it to your root layout:
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
// app/layout.tsx
|
|
266
|
+
import NDPRLayout from '@/components/ndpr-layout';
|
|
267
|
+
|
|
268
|
+
export default async function RootLayout({ children }) {
|
|
269
|
+
const session = await getServerSession();
|
|
270
|
+
return (
|
|
271
|
+
<html lang="en">
|
|
272
|
+
<body>
|
|
273
|
+
<NDPRLayout userId={session?.user?.id}>
|
|
274
|
+
{children}
|
|
275
|
+
</NDPRLayout>
|
|
276
|
+
</body>
|
|
277
|
+
</html>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Express Integration
|
|
285
|
+
|
|
286
|
+
### Mount the full compliance router
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import express from 'express';
|
|
290
|
+
import cookieParser from 'cookie-parser';
|
|
291
|
+
import { createNDPRRouter } from './ndpr/express';
|
|
292
|
+
|
|
293
|
+
const app = express();
|
|
294
|
+
app.use(express.json());
|
|
295
|
+
app.use(cookieParser()); // required for consent cookie fallback
|
|
296
|
+
|
|
297
|
+
// Mount all NDPR compliance routes under /api/ndpr
|
|
298
|
+
app.use('/api/ndpr', createNDPRRouter());
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
This mounts:
|
|
302
|
+
|
|
303
|
+
| Route | Module |
|
|
304
|
+
|---|---|
|
|
305
|
+
| `GET/POST/DELETE /api/ndpr/consent` | Consent management |
|
|
306
|
+
| `GET/POST/PATCH /api/ndpr/dsr` | Data subject rights |
|
|
307
|
+
| `GET/POST/PATCH /api/ndpr/breach` | Breach notification |
|
|
308
|
+
| `GET/POST/PATCH /api/ndpr/ropa` | Record of Processing Activities |
|
|
309
|
+
| `GET /api/ndpr/compliance` | Compliance score |
|
|
310
|
+
|
|
311
|
+
### Consent middleware (route protection)
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { requireConsent } from './ndpr/express/middleware/consent-check';
|
|
315
|
+
|
|
316
|
+
// Require marketing consent before sending a marketing email
|
|
317
|
+
app.post('/email/marketing', requireConsent('marketing'), sendEmailHandler);
|
|
318
|
+
|
|
319
|
+
// Require multiple consents — all must be granted
|
|
320
|
+
import { requireAllConsents } from './ndpr/express/middleware/consent-check';
|
|
321
|
+
app.post('/profile/analytics', requireAllConsents(['analytics', 'functional']), handler);
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Use individual routers (granular mounting)
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { consentRouter, dsrRouter } from './ndpr/express';
|
|
328
|
+
|
|
329
|
+
// Mount only the routes you need
|
|
330
|
+
app.use('/api/consent', consentRouter);
|
|
331
|
+
app.use('/api/dsr', dsrRouter);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Full Example
|
|
337
|
+
|
|
338
|
+
Below is the complete `layout-example.tsx` showing the toolkit wired up in a Next.js App Router layout with a server-backed consent adapter:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
'use client';
|
|
342
|
+
|
|
343
|
+
import React from 'react';
|
|
344
|
+
import { NDPRProvider } from '@tantainnovative/ndpr-toolkit/core';
|
|
345
|
+
import { NDPRConsent } from '@tantainnovative/ndpr-toolkit/presets';
|
|
346
|
+
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
|
|
347
|
+
|
|
348
|
+
export default function NDPRLayout({
|
|
349
|
+
children,
|
|
350
|
+
userId,
|
|
351
|
+
}: {
|
|
352
|
+
children: React.ReactNode;
|
|
353
|
+
userId?: string;
|
|
354
|
+
}) {
|
|
355
|
+
const subjectId = userId ?? 'anonymous';
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<NDPRProvider
|
|
359
|
+
organizationName="Your Company"
|
|
360
|
+
dpoEmail="dpo@yourcompany.ng"
|
|
361
|
+
>
|
|
362
|
+
{children}
|
|
363
|
+
|
|
364
|
+
<NDPRConsent
|
|
365
|
+
adapter={apiAdapter(`/api/consent?subjectId=${subjectId}`)}
|
|
366
|
+
/>
|
|
367
|
+
</NDPRProvider>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The `apiAdapter` hits your `/api/consent` route handler (from `src/nextjs/app-router/api/consent/route.ts`), which persists consent to PostgreSQL via Prisma or Drizzle.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Database Schema
|
|
377
|
+
|
|
378
|
+
### Tables
|
|
379
|
+
|
|
380
|
+
| Table | Description | NDPA reference |
|
|
381
|
+
|---|---|---|
|
|
382
|
+
| `ndpr_consent_records` | Immutable consent audit trail. `revokedAt` marks withdrawal — rows are never deleted. | §25–26 |
|
|
383
|
+
| `ndpr_dsr_requests` | Data subject rights requests. Tracks type, status, and 30-day response deadline. | Part IV §29–36 |
|
|
384
|
+
| `ndpr_breach_reports` | Breach incident records with 72-hour NDPC notification tracking. | §40 |
|
|
385
|
+
| `ndpr_processing_records` | Record of Processing Activities (ROPA). | Accountability principle |
|
|
386
|
+
| `ndpr_audit_log` | Append-only compliance event log. | §44 |
|
|
387
|
+
|
|
388
|
+
### Consent immutability
|
|
389
|
+
|
|
390
|
+
The consent table follows an immutable-audit pattern: when a subject updates or withdraws consent, the old row has `revokedAt` set and a new row is inserted. At most one row per `subjectId` has `revokedAt = NULL` at any time. This pattern ensures the full consent history is available for regulatory inspection without requiring separate audit log queries.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## NDPA Compliance References
|
|
395
|
+
|
|
396
|
+
| Module | NDPA provision |
|
|
397
|
+
|---|---|
|
|
398
|
+
| Consent | Sections 25–26 (lawful basis, consent withdrawal) |
|
|
399
|
+
| Data Subject Rights | Part IV, Sections 29–36 (access, erasure, portability, etc.) |
|
|
400
|
+
| Breach Notification | Section 40 (72-hour notification to NDPC) |
|
|
401
|
+
| ROPA | Accountability principle; Schedule 1, Part 1 |
|
|
402
|
+
| Audit Log | Section 44 (accountability and record-keeping) |
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tantainnovative/ndpr-recipes",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Backend recipes for @tantainnovative/ndpr-toolkit — Prisma schemas, API routes, and ORM adapters for NDPA compliance",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Abraham Esandayinze Tanta",
|
|
9
|
+
"url": "https://linkedin.com/in/mr-tanta"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/mr-tanta/ndpr-toolkit.git",
|
|
14
|
+
"directory": "packages/ndpr-recipes"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["ndpa", "ndpr", "nigeria", "data-protection", "prisma", "nextjs", "express"],
|
|
17
|
+
"files": [
|
|
18
|
+
"src/**/*",
|
|
19
|
+
"prisma/**/*",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model ConsentRecord {
|
|
11
|
+
id String @id @default(cuid())
|
|
12
|
+
subjectId String
|
|
13
|
+
consents Json
|
|
14
|
+
version String
|
|
15
|
+
method String
|
|
16
|
+
lawfulBasis String?
|
|
17
|
+
ipAddress String?
|
|
18
|
+
userAgent String?
|
|
19
|
+
createdAt DateTime @default(now())
|
|
20
|
+
revokedAt DateTime?
|
|
21
|
+
|
|
22
|
+
@@index([subjectId])
|
|
23
|
+
@@map("ndpr_consent_records")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
model DSRRequest {
|
|
27
|
+
id String @id @default(cuid())
|
|
28
|
+
type String
|
|
29
|
+
status String @default("pending")
|
|
30
|
+
subjectName String
|
|
31
|
+
subjectEmail String
|
|
32
|
+
subjectPhone String?
|
|
33
|
+
identifierType String
|
|
34
|
+
identifierValue String
|
|
35
|
+
description String?
|
|
36
|
+
internalNotes String?
|
|
37
|
+
assignedTo String?
|
|
38
|
+
submittedAt DateTime @default(now())
|
|
39
|
+
acknowledgedAt DateTime?
|
|
40
|
+
completedAt DateTime?
|
|
41
|
+
dueAt DateTime
|
|
42
|
+
|
|
43
|
+
@@index([status])
|
|
44
|
+
@@index([subjectEmail])
|
|
45
|
+
@@map("ndpr_dsr_requests")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
model BreachReport {
|
|
49
|
+
id String @id @default(cuid())
|
|
50
|
+
title String
|
|
51
|
+
description String
|
|
52
|
+
category String
|
|
53
|
+
severity String
|
|
54
|
+
status String @default("ongoing")
|
|
55
|
+
discoveredAt DateTime
|
|
56
|
+
occurredAt DateTime?
|
|
57
|
+
reportedAt DateTime @default(now())
|
|
58
|
+
ndpcNotifiedAt DateTime?
|
|
59
|
+
reporterName String
|
|
60
|
+
reporterEmail String
|
|
61
|
+
reporterDepartment String?
|
|
62
|
+
affectedSystems Json
|
|
63
|
+
dataTypes Json
|
|
64
|
+
estimatedAffected Int?
|
|
65
|
+
initialActions String?
|
|
66
|
+
ndpcNotificationSent Boolean @default(false)
|
|
67
|
+
|
|
68
|
+
@@index([status])
|
|
69
|
+
@@index([severity])
|
|
70
|
+
@@map("ndpr_breach_reports")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
model ProcessingRecord {
|
|
74
|
+
id String @id @default(cuid())
|
|
75
|
+
purpose String
|
|
76
|
+
lawfulBasis String
|
|
77
|
+
dataCategories Json
|
|
78
|
+
dataSubjects Json
|
|
79
|
+
recipients Json
|
|
80
|
+
retentionPeriod String
|
|
81
|
+
securityMeasures Json
|
|
82
|
+
transferCountries Json?
|
|
83
|
+
transferMechanism String?
|
|
84
|
+
dpiaConducted Boolean @default(false)
|
|
85
|
+
status String @default("active")
|
|
86
|
+
createdAt DateTime @default(now())
|
|
87
|
+
updatedAt DateTime @updatedAt
|
|
88
|
+
|
|
89
|
+
@@map("ndpr_processing_records")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
model ComplianceAuditLog {
|
|
93
|
+
id String @id @default(cuid())
|
|
94
|
+
module String
|
|
95
|
+
action String
|
|
96
|
+
entityId String
|
|
97
|
+
entityType String
|
|
98
|
+
changes Json?
|
|
99
|
+
performedBy String?
|
|
100
|
+
createdAt DateTime @default(now())
|
|
101
|
+
|
|
102
|
+
@@index([module, entityId])
|
|
103
|
+
@@map("ndpr_audit_log")
|
|
104
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle adapter for the Consent module.
|
|
3
|
+
*
|
|
4
|
+
* Implements StorageAdapter<ConsentSettings> backed by the `ndpr_consent_records`
|
|
5
|
+
* Drizzle table. Follows the same immutable-audit pattern as the Prisma adapter,
|
|
6
|
+
* as required by NDPA Section 25:
|
|
7
|
+
*
|
|
8
|
+
* - SAVE → soft-revokes any existing active record, then inserts a new row.
|
|
9
|
+
* - LOAD → returns the most recent non-revoked record for the subject.
|
|
10
|
+
* - REMOVE → soft-deletes by setting revokedAt on the active record (no hard deletes).
|
|
11
|
+
*
|
|
12
|
+
* Usage
|
|
13
|
+
* -----
|
|
14
|
+
* Copy this file into your project alongside your Drizzle client, then wire it
|
|
15
|
+
* into the toolkit hook:
|
|
16
|
+
*
|
|
17
|
+
* import { drizzle } from 'drizzle-orm/node-postgres';
|
|
18
|
+
* import { Pool } from 'pg';
|
|
19
|
+
* import { useConsent } from '@tantainnovative/ndpr-toolkit';
|
|
20
|
+
* import { drizzleConsentAdapter } from './adapters/drizzle-consent';
|
|
21
|
+
* import * as schema from './drizzle/schema';
|
|
22
|
+
*
|
|
23
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
24
|
+
* const db = drizzle(pool, { schema });
|
|
25
|
+
*
|
|
26
|
+
* function MyApp() {
|
|
27
|
+
* const adapter = drizzleConsentAdapter(db, session.userId);
|
|
28
|
+
* const { settings, updateConsent } = useConsent({ adapter });
|
|
29
|
+
* // ...
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Prerequisites
|
|
33
|
+
* -------------
|
|
34
|
+
* - The `ndpr_consent_records` table must exist (run your Drizzle migration).
|
|
35
|
+
* - `drizzle-orm` must be installed in your project.
|
|
36
|
+
* - `@tantainnovative/ndpr-toolkit` must be installed in your project.
|
|
37
|
+
*
|
|
38
|
+
* @module adapters/drizzle-consent
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { eq, and, isNull, desc } from 'drizzle-orm';
|
|
42
|
+
import type { StorageAdapter } from '@tantainnovative/ndpr-toolkit';
|
|
43
|
+
import type { ConsentSettings } from '@tantainnovative/ndpr-toolkit';
|
|
44
|
+
import { consentRecords } from '../drizzle/schema';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a Drizzle-backed StorageAdapter for ConsentSettings.
|
|
48
|
+
*
|
|
49
|
+
* @param db - Your Drizzle database instance (any driver — pg, neon, libsql, etc.)
|
|
50
|
+
* @param subjectId - Stable identifier for the data subject (e.g. user ID, session ID).
|
|
51
|
+
* This scopes all queries and is stored on every record.
|
|
52
|
+
* @returns A StorageAdapter<ConsentSettings> ready to pass to useConsent().
|
|
53
|
+
*/
|
|
54
|
+
export function drizzleConsentAdapter(
|
|
55
|
+
db: any,
|
|
56
|
+
subjectId: string,
|
|
57
|
+
): StorageAdapter<ConsentSettings> {
|
|
58
|
+
return {
|
|
59
|
+
/**
|
|
60
|
+
* Load the latest active (non-revoked) consent record for the subject.
|
|
61
|
+
*
|
|
62
|
+
* Uses Drizzle's type-safe query builder to select the most recent row
|
|
63
|
+
* where `subjectId` matches and `revokedAt` is NULL.
|
|
64
|
+
*
|
|
65
|
+
* Returns null if no record exists — the toolkit hook treats this as
|
|
66
|
+
* "no consent given" and displays the consent banner.
|
|
67
|
+
*/
|
|
68
|
+
async load(): Promise<ConsentSettings | null> {
|
|
69
|
+
const rows = await db
|
|
70
|
+
.select()
|
|
71
|
+
.from(consentRecords)
|
|
72
|
+
.where(and(eq(consentRecords.subjectId, subjectId), isNull(consentRecords.revokedAt)))
|
|
73
|
+
.orderBy(desc(consentRecords.createdAt))
|
|
74
|
+
.limit(1);
|
|
75
|
+
|
|
76
|
+
const record = rows[0];
|
|
77
|
+
if (!record) return null;
|
|
78
|
+
|
|
79
|
+
// Reconstruct the ConsentSettings shape expected by the toolkit hook.
|
|
80
|
+
return {
|
|
81
|
+
consents: record.consents as Record<string, boolean>,
|
|
82
|
+
timestamp: record.createdAt.getTime(),
|
|
83
|
+
version: record.version,
|
|
84
|
+
method: record.method,
|
|
85
|
+
hasInteracted: true,
|
|
86
|
+
lawfulBasis: (record.lawfulBasis as ConsentSettings['lawfulBasis']) ?? undefined,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Persist new consent settings.
|
|
92
|
+
*
|
|
93
|
+
* Step 1: Revoke all currently active records for this subject by setting
|
|
94
|
+
* revokedAt = now(). This preserves the audit trail as required by
|
|
95
|
+
* NDPA Section 25 — the old consent history is never erased.
|
|
96
|
+
*
|
|
97
|
+
* Step 2: Insert a fresh record representing the new consent state.
|
|
98
|
+
*
|
|
99
|
+
* Both steps are performed sequentially (not in a transaction by default).
|
|
100
|
+
* Wrap this in a Drizzle transaction if your database supports it and you
|
|
101
|
+
* need atomicity guarantees.
|
|
102
|
+
*/
|
|
103
|
+
async save(data: ConsentSettings): Promise<void> {
|
|
104
|
+
// Step 1: Revoke all currently active records for this subject.
|
|
105
|
+
await db
|
|
106
|
+
.update(consentRecords)
|
|
107
|
+
.set({ revokedAt: new Date() })
|
|
108
|
+
.where(and(eq(consentRecords.subjectId, subjectId), isNull(consentRecords.revokedAt)));
|
|
109
|
+
|
|
110
|
+
// Step 2: Insert the new consent record.
|
|
111
|
+
await db.insert(consentRecords).values({
|
|
112
|
+
subjectId,
|
|
113
|
+
consents: data.consents,
|
|
114
|
+
version: data.version,
|
|
115
|
+
method: data.method,
|
|
116
|
+
lawfulBasis: data.lawfulBasis ?? null,
|
|
117
|
+
// Pass ipAddress / userAgent by extending this adapter to accept
|
|
118
|
+
// a RequestContext parameter if you need to capture them.
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Revoke the current consent record for the subject without deleting it.
|
|
124
|
+
*
|
|
125
|
+
* Hard deletes are never performed so the compliance audit trail is preserved
|
|
126
|
+
* for NDPA Section 26 (right to withdraw consent) accountability purposes.
|
|
127
|
+
*/
|
|
128
|
+
async remove(): Promise<void> {
|
|
129
|
+
await db
|
|
130
|
+
.update(consentRecords)
|
|
131
|
+
.set({ revokedAt: new Date() })
|
|
132
|
+
.where(and(eq(consentRecords.subjectId, subjectId), isNull(consentRecords.revokedAt)));
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|