@@ -68,6 +68,17 @@ function jsonResponse(data, status, corsHeaders) {
6868 } ) ;
6969}
7070
71+ function csvResponse ( content , fileName , corsHeaders ) {
72+ return new Response ( content , {
73+ status : 200 ,
74+ headers : {
75+ "Content-Type" : "text/csv; charset=utf-8" ,
76+ "Content-Disposition" : `attachment; filename=\"${ fileName } \"` ,
77+ ...corsHeaders
78+ }
79+ } ) ;
80+ }
81+
7182function validatePayload ( body ) {
7283 const name = String ( body . name || "" ) . trim ( ) ;
7384 const email = String ( body . email || "" ) . trim ( ) ;
@@ -89,6 +100,142 @@ function validatePayload(body) {
89100 return { payload : { name, email, subject, message } } ;
90101}
91102
103+ function validateSmallGroupPayload ( body ) {
104+ const name = String ( body . name || "" ) . trim ( ) ;
105+ const area = String ( body . area || "" ) . trim ( ) ;
106+ const phone = String ( body . phone || "" ) . trim ( ) ;
107+ const message = String ( body . message || "" ) . trim ( ) ;
108+
109+ if ( ! name ) return { error : "Name is required." } ;
110+ if ( name . length > 120 ) return { error : "Name is too long." } ;
111+ if ( area . length > 120 ) return { error : "Preferred area is too long." } ;
112+ if ( phone . length > 50 ) return { error : "Phone is too long." } ;
113+ if ( message . length > 3000 ) return { error : "Comments are too long." } ;
114+
115+ return { payload : { name, area, phone, message } } ;
116+ }
117+
118+ function validateVolunteerPayload ( body ) {
119+ const name = String ( body . name || "" ) . trim ( ) ;
120+ const email = String ( body . email || "" ) . trim ( ) ;
121+ const ministry = String ( body . ministry || "" ) . trim ( ) ;
122+ const message = String ( body . message || "" ) . trim ( ) ;
123+
124+ if ( ! name || ! email ) {
125+ return { error : "Name and email are required." } ;
126+ }
127+
128+ if ( ! / ^ \S + @ \S + \. \S + $ / . test ( email ) ) {
129+ return { error : "Invalid email address." } ;
130+ }
131+
132+ if ( name . length > 120 ) return { error : "Name is too long." } ;
133+ if ( ministry . length > 120 ) return { error : "Ministry interest is too long." } ;
134+ if ( message . length > 3000 ) return { error : "Notes are too long." } ;
135+
136+ return { payload : { name, email, ministry, message } } ;
137+ }
138+
139+ function requireDb ( env ) {
140+ if ( ! env . DB ) {
141+ throw new Error ( "D1 binding DB is not configured." ) ;
142+ }
143+ return env . DB ;
144+ }
145+
146+ async function saveSmallGroupRegistration ( env , payload ) {
147+ const db = requireDb ( env ) ;
148+ await db . prepare (
149+ `INSERT INTO small_group_registrations (name, area, phone, message)
150+ VALUES (?1, ?2, ?3, ?4)`
151+ )
152+ . bind ( payload . name , payload . area , payload . phone , payload . message )
153+ . run ( ) ;
154+ }
155+
156+ async function saveVolunteerSignup ( env , payload ) {
157+ const db = requireDb ( env ) ;
158+ await db . prepare (
159+ `INSERT INTO volunteer_signups (name, email, ministry, message)
160+ VALUES (?1, ?2, ?3, ?4)`
161+ )
162+ . bind ( payload . name , payload . email , payload . ministry , payload . message )
163+ . run ( ) ;
164+ }
165+
166+ function csvEscape ( value ) {
167+ const raw = String ( value == null ? "" : value ) ;
168+ const escaped = raw . replace ( / " / g, '""' ) ;
169+ return `"${ escaped } "` ;
170+ }
171+
172+ function makeCsv ( header , rows ) {
173+ const lines = [ header . map ( csvEscape ) . join ( "," ) ] ;
174+ rows . forEach ( ( row ) => {
175+ lines . push ( row . map ( csvEscape ) . join ( "," ) ) ;
176+ } ) ;
177+ return lines . join ( "\n" ) ;
178+ }
179+
180+ async function exportRowsAsCsv ( env , type ) {
181+ const db = requireDb ( env ) ;
182+
183+ if ( type === "small_group" ) {
184+ const result = await db . prepare (
185+ `SELECT id, created_at, name, area, phone, message
186+ FROM small_group_registrations
187+ ORDER BY created_at DESC`
188+ ) . all ( ) ;
189+
190+ const rows = ( result . results || [ ] ) . map ( ( r ) => [
191+ r . id ,
192+ r . created_at ,
193+ r . name ,
194+ r . area ,
195+ r . phone ,
196+ r . message
197+ ] ) ;
198+
199+ return {
200+ fileName : "small-group-registrations.csv" ,
201+ content : makeCsv ( [ "id" , "created_at" , "name" , "area" , "phone" , "message" ] , rows )
202+ } ;
203+ }
204+
205+ if ( type === "volunteer" ) {
206+ const result = await db . prepare (
207+ `SELECT id, created_at, name, email, ministry, message
208+ FROM volunteer_signups
209+ ORDER BY created_at DESC`
210+ ) . all ( ) ;
211+
212+ const rows = ( result . results || [ ] ) . map ( ( r ) => [
213+ r . id ,
214+ r . created_at ,
215+ r . name ,
216+ r . email ,
217+ r . ministry ,
218+ r . message
219+ ] ) ;
220+
221+ return {
222+ fileName : "volunteer-signups.csv" ,
223+ content : makeCsv ( [ "id" , "created_at" , "name" , "email" , "ministry" , "message" ] , rows )
224+ } ;
225+ }
226+
227+ throw new Error ( "Unsupported export type." ) ;
228+ }
229+
230+ function isExportAuthorized ( request , env , url ) {
231+ const expected = String ( env . EXPORT_TOKEN || "" ) . trim ( ) ;
232+ if ( ! expected ) return false ;
233+
234+ const headerToken = String ( request . headers . get ( "x-admin-token" ) || "" ) . trim ( ) ;
235+ const queryToken = String ( url . searchParams . get ( "token" ) || "" ) . trim ( ) ;
236+ return headerToken === expected || queryToken === expected ;
237+ }
238+
92239async function sendEmailViaResend ( env , payload ) {
93240 const apiKey = env . RESEND_API_KEY ;
94241 if ( ! apiKey ) throw new Error ( "RESEND_API_KEY is not configured." ) ;
@@ -157,6 +304,32 @@ export default {
157304 return jsonResponse ( { ok : true } , 200 , corsHeaders ) ;
158305 }
159306
307+ // GET /api/export?type=small_group|volunteer&format=csv
308+ if ( method === "GET" && path === "/api/export" ) {
309+ if ( ! isExportAuthorized ( request , env , url ) ) {
310+ return jsonResponse ( { ok : false , error : "Unauthorized." } , 401 , corsHeaders ) ;
311+ }
312+
313+ const type = String ( url . searchParams . get ( "type" ) || "" ) . trim ( ) ;
314+ const format = String ( url . searchParams . get ( "format" ) || "csv" ) . trim ( ) . toLowerCase ( ) ;
315+
316+ if ( format !== "csv" ) {
317+ return jsonResponse ( { ok : false , error : "Only csv format is supported." } , 400 , corsHeaders ) ;
318+ }
319+
320+ if ( type !== "small_group" && type !== "volunteer" ) {
321+ return jsonResponse ( { ok : false , error : "Invalid export type." } , 400 , corsHeaders ) ;
322+ }
323+
324+ try {
325+ const exported = await exportRowsAsCsv ( env , type ) ;
326+ return csvResponse ( exported . content , exported . fileName , corsHeaders ) ;
327+ } catch ( err ) {
328+ console . error ( "CSV export failed:" , err . message ) ;
329+ return jsonResponse ( { ok : false , error : "Unable to export data." } , 500 , corsHeaders ) ;
330+ }
331+ }
332+
160333 // POST /api/contact
161334 if ( method === "POST" && path === "/api/contact" ) {
162335 let body ;
@@ -180,6 +353,52 @@ export default {
180353 }
181354 }
182355
356+ // POST /api/small-group
357+ if ( method === "POST" && path === "/api/small-group" ) {
358+ let body ;
359+ try {
360+ body = await request . json ( ) ;
361+ } catch ( _ ) {
362+ return jsonResponse ( { ok : false , error : "Invalid JSON body." } , 400 , corsHeaders ) ;
363+ }
364+
365+ const { payload, error } = validateSmallGroupPayload ( body ) ;
366+ if ( error ) {
367+ return jsonResponse ( { ok : false , error } , 400 , corsHeaders ) ;
368+ }
369+
370+ try {
371+ await saveSmallGroupRegistration ( env , payload ) ;
372+ return jsonResponse ( { ok : true } , 200 , corsHeaders ) ;
373+ } catch ( err ) {
374+ console . error ( "Small group save failed:" , err . message ) ;
375+ return jsonResponse ( { ok : false , error : "Unable to save registration." } , 500 , corsHeaders ) ;
376+ }
377+ }
378+
379+ // POST /api/volunteer
380+ if ( method === "POST" && path === "/api/volunteer" ) {
381+ let body ;
382+ try {
383+ body = await request . json ( ) ;
384+ } catch ( _ ) {
385+ return jsonResponse ( { ok : false , error : "Invalid JSON body." } , 400 , corsHeaders ) ;
386+ }
387+
388+ const { payload, error } = validateVolunteerPayload ( body ) ;
389+ if ( error ) {
390+ return jsonResponse ( { ok : false , error } , 400 , corsHeaders ) ;
391+ }
392+
393+ try {
394+ await saveVolunteerSignup ( env , payload ) ;
395+ return jsonResponse ( { ok : true } , 200 , corsHeaders ) ;
396+ } catch ( err ) {
397+ console . error ( "Volunteer save failed:" , err . message ) ;
398+ return jsonResponse ( { ok : false , error : "Unable to save signup." } , 500 , corsHeaders ) ;
399+ }
400+ }
401+
183402 return jsonResponse ( { ok : false , error : "Not found." } , 404 , corsHeaders ) ;
184403 }
185404} ;
0 commit comments