@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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Next.js App Router — DSR (Data Subject Rights) List/Create Route
3
+ *
4
+ * Handles listing and creating Data Subject Rights requests as required by
5
+ * NDPA Sections 34–38 (rights to access, rectification, erasure, portability,
6
+ * and objection). All requests must be acknowledged within 72 hours and
7
+ * fulfilled within 30 days under the Act.
8
+ *
9
+ * Endpoints
10
+ * ---------
11
+ * GET /api/dsr — List DSR requests (optional ?status= filter)
12
+ * POST /api/dsr — Submit a new DSR request
13
+ *
14
+ * How to use
15
+ * ----------
16
+ * Copy this file to `app/api/dsr/route.ts` in your Next.js project.
17
+ * For single-request operations see `app/api/dsr/[id]/route.ts`.
18
+ *
19
+ * @module dsr/route
20
+ */
21
+
22
+ import { NextRequest, NextResponse } from 'next/server';
23
+ import { PrismaClient } from '@prisma/client';
24
+
25
+ const prisma = new PrismaClient();
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // GET /api/dsr?status=pending
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /**
32
+ * List all DSR requests, optionally filtered by status.
33
+ *
34
+ * Supports the admin/DPO dashboard — returns requests ordered newest-first
35
+ * so overdue items surface quickly. The optional `status` query parameter
36
+ * lets you display only pending, in-progress, or completed requests.
37
+ *
38
+ * Query params:
39
+ * status (optional) — filter by request status: pending | in_progress | completed | rejected
40
+ *
41
+ * Returns 200 with an array of DSRRequest rows.
42
+ */
43
+ export async function GET(req: NextRequest) {
44
+ const status = req.nextUrl.searchParams.get('status');
45
+
46
+ const requests = await prisma.dSRRequest.findMany({
47
+ where: status ? { status } : undefined,
48
+ orderBy: { submittedAt: 'desc' },
49
+ });
50
+
51
+ return NextResponse.json(requests);
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // POST /api/dsr
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Submit a new Data Subject Rights request.
60
+ *
61
+ * The NDPA requires organisations to provide a clear mechanism for data
62
+ * subjects to exercise their rights (Section 34). This route:
63
+ * 1. Validates required fields.
64
+ * 2. Calculates the statutory 30-day deadline (dueAt).
65
+ * 3. Persists the request with status 'pending'.
66
+ * 4. Writes an audit log entry for accountability.
67
+ *
68
+ * Body (JSON):
69
+ * type (required) — access | rectification | erasure | portability | objection
70
+ * subjectName (required) — full name of the data subject
71
+ * subjectEmail (required) — email address of the data subject
72
+ * identifierType (required) — how the subject is identified (e.g. 'email', 'account_id')
73
+ * identifierValue (required) — the subject's identifier value
74
+ * subjectPhone (optional) — phone number
75
+ * description (optional) — additional context from the subject
76
+ *
77
+ * Returns 201 with the newly created DSRRequest row.
78
+ */
79
+ export async function POST(req: NextRequest) {
80
+ const body = await req.json();
81
+ const {
82
+ type,
83
+ subjectName,
84
+ subjectEmail,
85
+ subjectPhone,
86
+ identifierType,
87
+ identifierValue,
88
+ description,
89
+ } = body;
90
+
91
+ if (!type || !subjectName || !subjectEmail || !identifierType || !identifierValue) {
92
+ return NextResponse.json(
93
+ { error: 'type, subjectName, subjectEmail, identifierType, and identifierValue are required' },
94
+ { status: 400 },
95
+ );
96
+ }
97
+
98
+ // NDPA mandates a 30-day response window from the date of submission.
99
+ const dueAt = new Date();
100
+ dueAt.setDate(dueAt.getDate() + 30);
101
+
102
+ const request = await prisma.dSRRequest.create({
103
+ data: {
104
+ type,
105
+ subjectName,
106
+ subjectEmail,
107
+ subjectPhone: subjectPhone ?? null,
108
+ identifierType,
109
+ identifierValue,
110
+ description: description ?? null,
111
+ status: 'pending',
112
+ dueAt,
113
+ },
114
+ });
115
+
116
+ // Audit log for NDPA Section 44 accountability principle.
117
+ await prisma.complianceAuditLog.create({
118
+ data: {
119
+ module: 'dsr',
120
+ action: 'submitted',
121
+ entityId: request.id,
122
+ entityType: 'DSRRequest',
123
+ changes: { type, subjectEmail, status: 'pending' },
124
+ },
125
+ });
126
+
127
+ return NextResponse.json(request, { status: 201 });
128
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Next.js App Router — ROPA (Record of Processing Activities) Route
3
+ *
4
+ * Handles listing, creating, updating, and archiving processing activity records.
5
+ * Under the NDPA accountability principle (analogous to GDPR Article 30), data
6
+ * controllers with more than 250 employees — or that process sensitive data —
7
+ * must maintain a Record of Processing Activities (ROPA).
8
+ *
9
+ * Endpoints
10
+ * ---------
11
+ * GET /api/ropa — List all processing records (optional ?status= filter)
12
+ * POST /api/ropa — Create a new processing record
13
+ * PATCH /api/ropa — Update an existing processing record (body includes `id`)
14
+ * DELETE /api/ropa?id=xxx — Archive a processing record (soft delete)
15
+ *
16
+ * How to use
17
+ * ----------
18
+ * Copy this file to `app/api/ropa/route.ts` in your Next.js project.
19
+ *
20
+ * @module ropa/route
21
+ */
22
+
23
+ import { NextRequest, NextResponse } from 'next/server';
24
+ import { PrismaClient } from '@prisma/client';
25
+
26
+ const prisma = new PrismaClient();
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // GET /api/ropa?status=active
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * List all processing activity records.
34
+ *
35
+ * Returns records ordered by creation date ascending so the ROPA reads
36
+ * chronologically. Filter by status to distinguish active from archived entries.
37
+ *
38
+ * Query params:
39
+ * status (optional) — active | archived (default: returns all)
40
+ *
41
+ * Returns 200 with an array of ProcessingRecord rows.
42
+ */
43
+ export async function GET(req: NextRequest) {
44
+ const status = req.nextUrl.searchParams.get('status');
45
+
46
+ const records = await prisma.processingRecord.findMany({
47
+ where: status ? { status } : undefined,
48
+ orderBy: { createdAt: 'asc' },
49
+ });
50
+
51
+ return NextResponse.json(records);
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // POST /api/ropa
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Create a new processing activity record.
60
+ *
61
+ * Each record documents a single processing activity — for example, "Customer
62
+ * order processing" or "Marketing email campaigns". Together, all active records
63
+ * form the organisation's ROPA as required under the NDPA accountability principle.
64
+ *
65
+ * Body (JSON):
66
+ * purpose (required) — description of the processing activity
67
+ * lawfulBasis (required) — consent | contract | legal_obligation | vital_interests
68
+ * | public_task | legitimate_interests
69
+ * dataCategories (required) — array of data category labels (e.g. ['name', 'email'])
70
+ * dataSubjects (required) — array of subject category labels (e.g. ['customers'])
71
+ * recipients (required) — array of recipient labels (e.g. ['payment processor'])
72
+ * retentionPeriod (required) — human-readable retention policy (e.g. '7 years')
73
+ * securityMeasures (required) — array of security measures in place
74
+ * transferCountries (optional) — array of countries receiving cross-border transfers
75
+ * transferMechanism (optional) — legal mechanism for transfers (e.g. 'adequacy decision')
76
+ * dpiaConducted (optional) — whether a DPIA has been performed (default false)
77
+ *
78
+ * Returns 201 with the newly created ProcessingRecord row.
79
+ */
80
+ export async function POST(req: NextRequest) {
81
+ const body = await req.json();
82
+ const {
83
+ purpose,
84
+ lawfulBasis,
85
+ dataCategories,
86
+ dataSubjects,
87
+ recipients,
88
+ retentionPeriod,
89
+ securityMeasures,
90
+ transferCountries,
91
+ transferMechanism,
92
+ dpiaConducted,
93
+ } = body;
94
+
95
+ if (
96
+ !purpose ||
97
+ !lawfulBasis ||
98
+ !Array.isArray(dataCategories) ||
99
+ !Array.isArray(dataSubjects) ||
100
+ !Array.isArray(recipients) ||
101
+ !retentionPeriod ||
102
+ !Array.isArray(securityMeasures)
103
+ ) {
104
+ return NextResponse.json(
105
+ {
106
+ error:
107
+ 'purpose, lawfulBasis, dataCategories, dataSubjects, recipients, retentionPeriod, and securityMeasures are required',
108
+ },
109
+ { status: 400 },
110
+ );
111
+ }
112
+
113
+ const record = await prisma.processingRecord.create({
114
+ data: {
115
+ purpose,
116
+ lawfulBasis,
117
+ dataCategories,
118
+ dataSubjects,
119
+ recipients,
120
+ retentionPeriod,
121
+ securityMeasures,
122
+ transferCountries: transferCountries ?? null,
123
+ transferMechanism: transferMechanism ?? null,
124
+ dpiaConducted: dpiaConducted ?? false,
125
+ status: 'active',
126
+ },
127
+ });
128
+
129
+ await prisma.complianceAuditLog.create({
130
+ data: {
131
+ module: 'ropa',
132
+ action: 'created',
133
+ entityId: record.id,
134
+ entityType: 'ProcessingRecord',
135
+ changes: { purpose, lawfulBasis, status: 'active' },
136
+ },
137
+ });
138
+
139
+ return NextResponse.json(record, { status: 201 });
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // PATCH /api/ropa
144
+ // ---------------------------------------------------------------------------
145
+
146
+ /**
147
+ * Update an existing processing activity record.
148
+ *
149
+ * Call this when a processing activity changes — e.g. a new data category
150
+ * is added, the retention period changes, or a DPIA is conducted. Keeping
151
+ * the ROPA up to date is part of the NDPA accountability obligation.
152
+ *
153
+ * Body (JSON):
154
+ * id (required) — ID of the ProcessingRecord to update
155
+ * ... (all other fields from POST are optional — only send what changed)
156
+ *
157
+ * Returns 200 with the updated ProcessingRecord row.
158
+ */
159
+ export async function PATCH(req: NextRequest) {
160
+ const body = await req.json();
161
+ const { id, ...fields } = body;
162
+
163
+ if (!id) {
164
+ return NextResponse.json({ error: 'id is required in the request body' }, { status: 400 });
165
+ }
166
+
167
+ const existing = await prisma.processingRecord.findUnique({ where: { id } });
168
+ if (!existing) {
169
+ return NextResponse.json({ error: 'Processing record not found' }, { status: 404 });
170
+ }
171
+
172
+ const updated = await prisma.processingRecord.update({
173
+ where: { id },
174
+ data: fields,
175
+ });
176
+
177
+ await prisma.complianceAuditLog.create({
178
+ data: {
179
+ module: 'ropa',
180
+ action: 'updated',
181
+ entityId: id,
182
+ entityType: 'ProcessingRecord',
183
+ changes: fields,
184
+ },
185
+ });
186
+
187
+ return NextResponse.json(updated);
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // DELETE /api/ropa?id=xxx
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Archive a processing activity record (soft delete).
196
+ *
197
+ * Records are never hard-deleted — archiving sets status to 'archived' so
198
+ * the historical ROPA remains available for regulatory review. This supports
199
+ * the NDPA accountability principle requirement to demonstrate compliance
200
+ * over time, not just in the present.
201
+ *
202
+ * Query params:
203
+ * id (required) — ID of the ProcessingRecord to archive
204
+ *
205
+ * Returns 200 `{ success: true }` when complete.
206
+ */
207
+ export async function DELETE(req: NextRequest) {
208
+ const id = req.nextUrl.searchParams.get('id');
209
+
210
+ if (!id) {
211
+ return NextResponse.json({ error: 'id query parameter required' }, { status: 400 });
212
+ }
213
+
214
+ const existing = await prisma.processingRecord.findUnique({ where: { id } });
215
+ if (!existing) {
216
+ return NextResponse.json({ error: 'Processing record not found' }, { status: 404 });
217
+ }
218
+
219
+ await prisma.processingRecord.update({
220
+ where: { id },
221
+ data: { status: 'archived' },
222
+ });
223
+
224
+ await prisma.complianceAuditLog.create({
225
+ data: {
226
+ module: 'ropa',
227
+ action: 'archived',
228
+ entityId: id,
229
+ entityType: 'ProcessingRecord',
230
+ },
231
+ });
232
+
233
+ return NextResponse.json({ success: true });
234
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Example: Full NDPA compliance wiring in a Next.js App Router layout.
3
+ *
4
+ * This file shows how to integrate the @tantainnovative/ndpr-toolkit consent
5
+ * banner with a persistent backend using the Prisma adapter. The pattern works
6
+ * for both the built-in Prisma adapter and the Drizzle adapter — swap out
7
+ * `prismaConsentAdapter` for `drizzleConsentAdapter` if you use Drizzle.
8
+ *
9
+ * What this example covers
10
+ * ------------------------
11
+ * - Wrapping your app with NDPRProvider so all child components can access the
12
+ * consent context via `useConsent()`.
13
+ * - Rendering the NDPRConsent banner with a server-backed storage adapter so
14
+ * consent decisions survive page refreshes and browser restarts.
15
+ * - Wiring the apiAdapter to your Next.js API route (`/api/consent`) for a
16
+ * fully decoupled client ↔ server consent flow.
17
+ *
18
+ * Copy this file, adapt the userId retrieval to your auth system, and you're done.
19
+ *
20
+ * @see prisma/schema.prisma — for the database schema
21
+ * @see src/nextjs/app-router/api/ — for the API route implementations
22
+ * @see src/adapters/prisma-consent.ts — for the server-side Prisma adapter
23
+ *
24
+ * @module nextjs/app-router/layout-example
25
+ */
26
+
27
+ 'use client';
28
+
29
+ import React from 'react';
30
+ import { NDPRProvider } from '@tantainnovative/ndpr-toolkit/core';
31
+ import { NDPRConsent } from '@tantainnovative/ndpr-toolkit/presets';
32
+ import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Types
36
+ // ---------------------------------------------------------------------------
37
+
38
+ interface NDPRLayoutProps {
39
+ children: React.ReactNode;
40
+ /**
41
+ * Optional: pass the authenticated user's ID down from your root layout.
42
+ * In a real app this comes from your session (NextAuth, Clerk, Supabase Auth, etc.).
43
+ *
44
+ * If no userId is available (unauthenticated visitor), pass a stable anonymous
45
+ * identifier instead — e.g. a UUID stored in a cookie. Consent must still be
46
+ * obtained even for non-authenticated users.
47
+ */
48
+ userId?: string;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Layout component
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /**
56
+ * NDPRLayout — wraps your app with consent management and the banner UI.
57
+ *
58
+ * Recommended placement: inside your root `app/layout.tsx`, wrapped around
59
+ * the `{children}` slot. Example:
60
+ *
61
+ * // app/layout.tsx
62
+ * import { NDPRLayout } from '@/components/ndpr-layout';
63
+ *
64
+ * export default async function RootLayout({ children }) {
65
+ * const session = await getServerSession();
66
+ * return (
67
+ * <html lang="en">
68
+ * <body>
69
+ * <NDPRLayout userId={session?.user?.id}>
70
+ * {children}
71
+ * </NDPRLayout>
72
+ * </body>
73
+ * </html>
74
+ * );
75
+ * }
76
+ *
77
+ * Note: This component is marked 'use client' because NDPRProvider and the
78
+ * consent banner both require client-side interactivity. The session/userId
79
+ * should be resolved in the parent Server Component and passed as a prop.
80
+ */
81
+ export default function NDPRLayout({ children, userId }: NDPRLayoutProps) {
82
+ // ---------------------------------------------------------------------------
83
+ // Subject identification
84
+ // ---------------------------------------------------------------------------
85
+
86
+ // In a real application, replace this with your actual auth system:
87
+ // const { data: session } = useSession(); // NextAuth
88
+ // const { user } = useUser(); // Clerk
89
+ // const { session } = useSessionContext(); // Supabase Auth
90
+ //
91
+ // For anonymous visitors: generate and persist a UUID in localStorage or a cookie
92
+ // so consent decisions are stable across page reloads.
93
+ const subjectId = userId ?? 'anonymous';
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Adapter
97
+ // ---------------------------------------------------------------------------
98
+
99
+ // apiAdapter points at the Next.js API route that wraps the Prisma/Drizzle adapter.
100
+ // The route is implemented in `src/nextjs/app-router/api/consent/route.ts`.
101
+ //
102
+ // For direct server-side use (SSR / Server Components), import and call
103
+ // `prismaConsentAdapter(prisma, subjectId)` directly instead.
104
+ const consentStorageAdapter = apiAdapter(`/api/consent?subjectId=${subjectId}`);
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Render
108
+ // ---------------------------------------------------------------------------
109
+
110
+ return (
111
+ <NDPRProvider
112
+ /**
113
+ * Your organisation's legal name — displayed in the consent banner headline
114
+ * and in the Data Subject Rights portal.
115
+ */
116
+ organizationName="Your Company"
117
+ /**
118
+ * DPO or privacy contact email — shown to data subjects who want to exercise
119
+ * their rights or ask questions about data processing.
120
+ */
121
+ dpoEmail="dpo@yourcompany.ng"
122
+ >
123
+ {/* Render your application's pages and components */}
124
+ {children}
125
+
126
+ {/*
127
+ * NDPRConsent — the NDPA-compliant consent banner.
128
+ *
129
+ * The `adapter` prop connects the banner to your database so consent
130
+ * decisions are persisted server-side and survive page refreshes.
131
+ *
132
+ * On first visit: the banner is displayed and the subject can accept,
133
+ * decline, or customise individual consent categories.
134
+ *
135
+ * On subsequent visits: the adapter loads the stored decision and the
136
+ * banner is hidden (no flicker, no double-prompt).
137
+ *
138
+ * The banner automatically handles:
139
+ * - NDPA §25 — recording the lawful basis and consent version
140
+ * - NDPA §26 — providing a clear withdrawal mechanism
141
+ * - Immutable audit trail — every change is stored as a new record
142
+ */}
143
+ <NDPRConsent adapter={consentStorageAdapter} />
144
+ </NDPRProvider>
145
+ );
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Alternative: using the Drizzle adapter directly (no API route needed)
150
+ // ---------------------------------------------------------------------------
151
+ //
152
+ // If you prefer to use the Drizzle adapter on the server side inside a
153
+ // Server Component or a Route Handler, here is how to wire it:
154
+ //
155
+ // import { drizzle } from 'drizzle-orm/node-postgres';
156
+ // import { Pool } from 'pg';
157
+ // import { drizzleConsentAdapter } from '@/adapters/drizzle-consent';
158
+ // import * as schema from '@/drizzle/schema';
159
+ //
160
+ // const pool = new Pool({ connectionString: process.env.DATABASE_URL });
161
+ // const db = drizzle(pool, { schema });
162
+ //
163
+ // // In a Server Component or API Route:
164
+ // const adapter = drizzleConsentAdapter(db, subjectId);
165
+ // const settings = await adapter.load();
166
+ //
167
+ // ---------------------------------------------------------------------------
168
+ // Alternative: using the Prisma adapter directly (no API route needed)
169
+ // ---------------------------------------------------------------------------
170
+ //
171
+ // import { PrismaClient } from '@prisma/client';
172
+ // import { prismaConsentAdapter } from '@/adapters/prisma-consent';
173
+ //
174
+ // const prisma = new PrismaClient();
175
+ //
176
+ // // In a Server Component or API Route:
177
+ // const adapter = prismaConsentAdapter(prisma, subjectId);
178
+ // const settings = await adapter.load();
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Next.js App Router — Consent Middleware
3
+ *
4
+ * Provides a reusable consent-gate helper that can be called inside any
5
+ * App Router route handler (or in Next.js middleware.ts at the edge) to verify
6
+ * that a data subject has granted the required consent before the route proceeds.
7
+ *
8
+ * NDPA Section 25 requires that processing based on consent only happens when
9
+ * the data subject has freely given specific, informed, and unambiguous consent.
10
+ * This helper enforces that requirement at the HTTP layer.
11
+ *
12
+ * How to use
13
+ * ----------
14
+ * Call `consentMiddleware(req, 'marketing')` at the top of any route handler
15
+ * that requires a specific consent type. If the function returns a Response,
16
+ * return it immediately; otherwise continue with your logic:
17
+ *
18
+ * import { consentMiddleware } from '../middleware';
19
+ *
20
+ * export async function POST(req: NextRequest) {
21
+ * const guard = await consentMiddleware(req, 'marketing');
22
+ * if (guard) return guard; // blocked — return 403
23
+ *
24
+ * // subject has consented — proceed
25
+ * }
26
+ *
27
+ * Subject identification
28
+ * ----------------------
29
+ * The helper looks for a subject identifier in two places (in priority order):
30
+ * 1. The `x-subject-id` request header (set by your auth middleware or JWT)
31
+ * 2. The `subject_id` cookie (set by your consent banner on the client)
32
+ *
33
+ * @module middleware
34
+ */
35
+
36
+ import { NextRequest, NextResponse } from 'next/server';
37
+ import { PrismaClient } from '@prisma/client';
38
+
39
+ const prisma = new PrismaClient();
40
+
41
+ /**
42
+ * Verify that the requesting data subject has granted the specified consent type.
43
+ *
44
+ * @param req - The incoming NextRequest
45
+ * @param requiredConsent - The consent category key to check (e.g. 'marketing', 'analytics')
46
+ * @returns null if the subject has consented (allow through), or a NextResponse with
47
+ * status 403 and an error body if the check fails (block the request).
48
+ */
49
+ export async function consentMiddleware(
50
+ req: NextRequest,
51
+ requiredConsent: string,
52
+ ): Promise<NextResponse | null> {
53
+ // Resolve the subject identifier from the header or cookie.
54
+ const subjectId =
55
+ req.headers.get('x-subject-id') ?? req.cookies.get('subject_id')?.value ?? null;
56
+
57
+ if (!subjectId) {
58
+ return NextResponse.json(
59
+ { error: 'Consent verification required' },
60
+ { status: 403 },
61
+ );
62
+ }
63
+
64
+ // Load the most recent active consent record for this subject.
65
+ const record = await prisma.consentRecord.findFirst({
66
+ where: { subjectId, revokedAt: null },
67
+ orderBy: { createdAt: 'desc' },
68
+ });
69
+
70
+ if (!record) {
71
+ return NextResponse.json(
72
+ { error: 'No consent on record' },
73
+ { status: 403 },
74
+ );
75
+ }
76
+
77
+ // Check that the specific consent type has been granted.
78
+ const consents = record.consents as Record<string, boolean>;
79
+
80
+ if (!consents[requiredConsent]) {
81
+ return NextResponse.json(
82
+ { error: `Consent for "${requiredConsent}" not granted` },
83
+ { status: 403 },
84
+ );
85
+ }
86
+
87
+ // Subject has consented — allow the request to proceed.
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Higher-order helper: wraps a route handler with a consent gate.
93
+ *
94
+ * This is an alternative to calling consentMiddleware() manually at the top
95
+ * of every handler. Use it when all methods on a route require the same consent.
96
+ *
97
+ * export const POST = withConsent('marketing', async (req) => {
98
+ * // marketing consent guaranteed here
99
+ * });
100
+ *
101
+ * @param requiredConsent - The consent category to enforce
102
+ * @param handler - The route handler to wrap
103
+ */
104
+ export function withConsent(
105
+ requiredConsent: string,
106
+ handler: (req: NextRequest, ctx: any) => Promise<NextResponse>,
107
+ ) {
108
+ return async (req: NextRequest, ctx: any): Promise<NextResponse> => {
109
+ const guard = await consentMiddleware(req, requiredConsent);
110
+ if (guard) return guard;
111
+ return handler(req, ctx);
112
+ };
113
+ }