import { supabaseServiceRole } from '@/lib/supabase'; import type { APIRoute } from 'astro'; import { jwtVerify } from 'jose'; const SUPABASE_JWT_SECRET = import.meta.env.SUPABASE_JWT_SECRET; type CrudOperation = { id: string; table: string; op: 'PUT' | 'PATCH' | 'DELETE'; opData?: Record; }; const ALLOWED_TABLES = [ 'profiles', 'days', 'food_logs', 'food_log_preds', 'memo_loggers', 'memo_logs', ]; const SOFT_DELETE_TABLES = ['food_logs', 'memo_loggers', 'memo_logs']; const JSON_FIELDS: Record = { profiles: ['health_conditions', 'food_restrictions', 'food_preferences', 'add_log_shortcuts'], food_logs: ['ingredients', 'nutrition', 'bounding_box'], food_log_preds: ['ingredients', 'nutrition'], days: ['recommended_ingredients', 'recommended_foods', 'nutrition_target', 'nutrition_consumed'], memo_loggers: ['options'], }; const LOCAL_ONLY_FIELDS = ['photo_uri', 'local_pending_upload']; export const POST: APIRoute = async ({ request }) => { const headers = { 'Content-Type': 'application/json' }; try { const authHeader = request.headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return new Response(JSON.stringify({ success: false, message: 'Missing authorization' }), { status: 401, headers, }); } const token = authHeader.slice(7); const secret = new TextEncoder().encode(SUPABASE_JWT_SECRET); const { payload } = await jwtVerify(token, secret, { audience: 'authenticated' }); const profileId = payload.sub as string; const { operations } = (await request.json()) as { operations: CrudOperation[] }; if (operations.length > 1000) { return new Response(JSON.stringify({ success: false, message: 'Batch size exceeds limit' }), { status: 400, headers, }); } for (const op of operations) { if (!ALLOWED_TABLES.includes(op.table)) { return new Response( JSON.stringify({ success: false, message: `Table not allowed: ${op.table}` }), { status: 400, headers } ); } const ownership = await verifyOwnership(op, profileId); if (ownership === 'denied') { return new Response(JSON.stringify({ success: false, message: 'Unauthorized operation' }), { status: 403, headers, }); } if (ownership === 'demo') { continue; } await applyOperation(op); } return new Response(JSON.stringify({ success: true }), { status: 200, headers }); } catch { return new Response(JSON.stringify({ success: false, message: 'Upload failed' }), { status: 500, headers, }); } }; async function verifyOwnership( op: CrudOperation, profileId: string ): Promise<'owned' | 'demo' | 'denied'> { if (op.table === 'profiles') { return op.id === profileId ? 'owned' : 'denied'; } const ownerId = op.opData?.profile_id as string | undefined; if (ownerId) { if (ownerId === 'demo') return 'demo'; return ownerId === profileId ? 'owned' : 'denied'; } const { data } = await supabaseServiceRole .from(op.table) .select('profile_id') .eq('id', op.id) .single(); if (!data) { return op.op === 'PUT' ? 'owned' : 'demo'; } if (data.profile_id === 'demo') return 'demo'; return data.profile_id === profileId ? 'owned' : 'denied'; } function preparePayload(table: string, data: Record): Record { const payload = { ...data }; for (const field of LOCAL_ONLY_FIELDS) { delete payload[field]; } for (const field of JSON_FIELDS[table] ?? []) { if (typeof payload[field] === 'string') { payload[field] = JSON.parse(payload[field] as string); } } return payload; } async function applyOperation(op: CrudOperation): Promise { const payload = preparePayload(op.table, op.opData ?? {}); if (op.op === 'PUT') { const { error } = await supabaseServiceRole .from(op.table) .upsert({ id: op.id, ...payload }, { onConflict: 'id' }); if (error) throw error; } else if (op.op === 'PATCH') { const { error } = await supabaseServiceRole.from(op.table).update(payload).eq('id', op.id); if (error) throw error; } else if (op.op === 'DELETE') { if (SOFT_DELETE_TABLES.includes(op.table)) { const { error } = await supabaseServiceRole .from(op.table) .update({ deleted_at: new Date().toISOString() }) .eq('id', op.id); if (error) throw error; } else { const { error } = await supabaseServiceRole.from(op.table).delete().eq('id', op.id); if (error) throw error; } } }