@@ -12,6 +12,36 @@ export const prerender = false // Force SSR for this endpoint
1212
1313const ROUTE = '/api/gdpr/consent'
1414
15+ const CONSENT_PURPOSES = [ 'contact' , 'marketing' , 'analytics' , 'downloads' ] as const
16+ type ConsentPurpose = ( typeof CONSENT_PURPOSES ) [ number ]
17+
18+ const CONSENT_SOURCES = [ 'contact_form' , 'newsletter_form' , 'download_form' , 'cookies_modal' , 'preferences_page' ] as const
19+ type ConsentSource = ( typeof CONSENT_SOURCES ) [ number ]
20+
21+ const DEFAULT_SOURCE : ConsentSource = 'cookies_modal'
22+ const DEFAULT_USER_AGENT = 'unknown'
23+
24+ const isConsentPurpose = ( value : unknown ) : value is ConsentPurpose =>
25+ typeof value === 'string' && CONSENT_PURPOSES . includes ( value as ConsentPurpose )
26+
27+ const isConsentSource = ( value : unknown ) : value is ConsentSource =>
28+ typeof value === 'string' && CONSENT_SOURCES . includes ( value as ConsentSource )
29+
30+ const sanitizePurposes = ( purposes : unknown ) : ConsentPurpose [ ] =>
31+ Array . isArray ( purposes ) ? purposes . filter ( isConsentPurpose ) : [ ]
32+
33+ const sanitizeSource = ( source : unknown ) : ConsentSource => ( isConsentSource ( source ) ? source : DEFAULT_SOURCE )
34+
35+ const normalizeNullableString = ( value ?: string | null ) : string | null => {
36+ if ( typeof value !== 'string' ) {
37+ return null
38+ }
39+ const trimmed = value . trim ( )
40+ return trimmed . length > 0 ? trimmed : null
41+ }
42+
43+ const normalizeUserAgent = ( value ?: string | null ) : string => normalizeNullableString ( value ) ?? DEFAULT_USER_AGENT
44+
1545type ConsentRecordRow = {
1646 id : string
1747 data_subject_id : string
@@ -46,33 +76,63 @@ const buildRateLimitError = (reset: number | undefined, message?: string) => {
4676 } )
4777}
4878
49- const mapConsentRecord = ( record : ConsentRecordRow ) : ConsentResponse [ 'record' ] => ( {
50- id : record . id ,
51- DataSubjectId : record . data_subject_id ,
52- email : record . email ,
53- purposes : record . purposes ,
54- timestamp : record . timestamp ,
55- source : record . source ,
56- userAgent : record . user_agent ,
57- ipAddress : record . ip_address ,
58- privacyPolicyVersion : record . privacy_policy_version ,
59- consentText : record . consent_text ,
60- verified : record . verified ,
61- } )
62-
63- const buildMockConsentRecord = ( body : ConsentRequest ) : ConsentResponse [ 'record' ] => ( {
64- id : randomUUID ( ) ,
65- DataSubjectId : body . DataSubjectId ,
66- email : body . email ?. toLowerCase ( ) . trim ( ) ?? null ,
67- purposes : body . purposes ,
68- timestamp : new Date ( ) . toISOString ( ) ,
69- source : body . source ?? null ,
70- userAgent : body . userAgent ?? null ,
71- ipAddress : body . ipAddress ?? null ,
72- privacyPolicyVersion : getPrivacyPolicyVersion ( ) ,
73- consentText : body . consentText ?? null ,
74- verified : body . verified ?? false ,
75- } )
79+ const mapConsentRecord = ( record : ConsentRecordRow ) : ConsentResponse [ 'record' ] => {
80+ const normalizedEmail = normalizeNullableString ( record . email )
81+ const normalizedIpAddress = normalizeNullableString ( record . ip_address )
82+ const normalizedConsentText = normalizeNullableString ( record . consent_text )
83+
84+ const mapped : ConsentResponse [ 'record' ] = {
85+ id : record . id ,
86+ DataSubjectId : record . data_subject_id ,
87+ purposes : sanitizePurposes ( record . purposes ) ,
88+ timestamp : record . timestamp ,
89+ source : sanitizeSource ( record . source ) ,
90+ userAgent : normalizeUserAgent ( record . user_agent ) ,
91+ privacyPolicyVersion : record . privacy_policy_version ?? getPrivacyPolicyVersion ( ) ,
92+ verified : record . verified ,
93+ }
94+
95+ if ( normalizedEmail ) {
96+ mapped . email = normalizedEmail
97+ }
98+ if ( normalizedIpAddress ) {
99+ mapped . ipAddress = normalizedIpAddress
100+ }
101+ if ( normalizedConsentText ) {
102+ mapped . consentText = normalizedConsentText
103+ }
104+
105+ return mapped
106+ }
107+
108+ const buildMockConsentRecord = ( body : ConsentRequest ) : ConsentResponse [ 'record' ] => {
109+ const normalizedEmail = normalizeNullableString ( body . email ?? null )
110+ const normalizedIpAddress = normalizeNullableString ( body . ipAddress ?? null )
111+ const normalizedConsentText = normalizeNullableString ( body . consentText ?? null )
112+
113+ const mockRecord : ConsentResponse [ 'record' ] = {
114+ id : randomUUID ( ) ,
115+ DataSubjectId : body . DataSubjectId ,
116+ purposes : sanitizePurposes ( body . purposes ) ,
117+ timestamp : new Date ( ) . toISOString ( ) ,
118+ source : sanitizeSource ( body . source ) ,
119+ userAgent : normalizeUserAgent ( body . userAgent ) ,
120+ privacyPolicyVersion : getPrivacyPolicyVersion ( ) ,
121+ verified : body . verified ?? false ,
122+ }
123+
124+ if ( normalizedEmail ) {
125+ mockRecord . email = normalizedEmail
126+ }
127+ if ( normalizedIpAddress ) {
128+ mockRecord . ipAddress = normalizedIpAddress
129+ }
130+ if ( normalizedConsentText ) {
131+ mockRecord . consentText = normalizedConsentText
132+ }
133+
134+ return mockRecord
135+ }
76136
77137const buildErrorResponse = (
78138 error : unknown ,
@@ -130,19 +190,26 @@ export const POST: APIRoute = async ({ request, cookies, clientAddress }) => {
130190 } )
131191 }
132192
193+ const normalizedEmail = normalizeNullableString ( body . email ?? null )
194+ const normalizedPurposes = sanitizePurposes ( body . purposes )
195+ const normalizedSource = sanitizeSource ( body . source )
196+ const normalizedUserAgent = normalizeUserAgent ( body . userAgent )
197+ const normalizedIpAddress = normalizeNullableString ( body . ipAddress ?? null )
198+ const normalizedConsentText = normalizeNullableString ( body . consentText ?? null )
199+
133200 let record : ConsentResponse [ 'record' ]
134201 try {
135202 const { data, error } = await supabaseAdmin
136203 . from ( 'consent_records' )
137204 . insert ( {
138205 data_subject_id : body . DataSubjectId ,
139- email : body . email ?. toLowerCase ( ) . trim ( ) ,
140- purposes : body . purposes ,
141- source : body . source ,
142- user_agent : body . userAgent ,
143- ip_address : body . ipAddress ,
206+ email : normalizedEmail ,
207+ purposes : normalizedPurposes ,
208+ source : normalizedSource ,
209+ user_agent : normalizedUserAgent ,
210+ ip_address : normalizedIpAddress ,
144211 privacy_policy_version : getPrivacyPolicyVersion ( ) ,
145- consent_text : body . consentText ,
212+ consent_text : normalizedConsentText ,
146213 verified : body . verified ?? false ,
147214 } )
148215 . select ( )
@@ -232,19 +299,7 @@ export const GET: APIRoute = async ({ clientAddress, url, request, cookies }) =>
232299 } )
233300 }
234301
235- const records = data . map ( record => ( {
236- id : record . id ,
237- DataSubjectId : record . data_subject_id ,
238- email : record . email ,
239- purposes : record . purposes ,
240- timestamp : record . timestamp ,
241- source : record . source ,
242- userAgent : record . user_agent ,
243- ipAddress : record . ip_address ,
244- privacyPolicyVersion : record . privacy_policy_version ,
245- consentText : record . consent_text ,
246- verified : record . verified
247- } ) )
302+ const records = data . map ( record => mapConsentRecord ( record as ConsentRecordRow ) )
248303
249304 return jsonResponse (
250305 {
0 commit comments