import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; interface NotificationRow { id: string; user_id: string; type: 'follow' | 'appreciate' | 'comment' | 'chain' | 'mention' | 'follow_request' | 'new_follower' | 'request_accepted'; actor_id: string; post_id: string | null; comment_id: string | null; metadata: Record; is_read: boolean; created_at: string; } interface NotificationResponse extends NotificationRow { actor: { id: string; handle: string; display_name: string; avatar_url: string | null; }; post?: { id: string; body: string; } | null; } Deno.serve(async (req) => { // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } try { const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? ''; const anonKey = Deno.env.get('SUPABASE_ANON_KEY') ?? ''; const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''; const supabaseClient = createClient(supabaseUrl, anonKey, { global: { headers: { Authorization: req.headers.get('Authorization') ?? '', apikey: anonKey, }, }, }); const adminClient = createClient(supabaseUrl, serviceRoleKey); // Get authenticated user (explicit token from header) const authHeader = req.headers.get('Authorization') ?? req.headers.get('authorization') ?? ''; const token = authHeader.startsWith('Bearer ') ? authHeader.slice('Bearer '.length) : authHeader; const { data: { user }, error: authError, } = await supabaseClient.auth.getUser(token || undefined); if (authError || !user) { console.error('Auth error:', authError?.message ?? 'No user found'); console.error('Auth header present:', !!authHeader, 'Length:', authHeader?.length ?? 0); return new Response(JSON.stringify({ error: 'Unauthorized', code: 401, message: authError?.message ?? 'Invalid JWT' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } const url = new URL(req.url); const limit = parseInt(url.searchParams.get('limit') || '20'); const offset = parseInt(url.searchParams.get('offset') || '0'); const unreadOnly = url.searchParams.get('unread_only') === 'true'; const includeArchived = url.searchParams.get('include_archived') === 'true'; // Handle different HTTP methods if (req.method === 'GET') { // Build query let query = adminClient .from('notifications') .select(` id, user_id, type, actor_id, post_id, comment_id, metadata, is_read, archived_at, created_at, actor:actor_id ( id, handle, display_name, avatar_url ), post:post_id ( id, body ) `) .eq('user_id', user.id) .order('created_at', { ascending: false }) .range(offset, offset + limit - 1); if (!includeArchived) { query = query.is('archived_at', null); } if (unreadOnly) { query = query.eq('is_read', false); } const { data: notifications, error } = await query; if (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify({ notifications }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } if (req.method === 'PATCH' || req.method === 'POST') { // Mark notifications as read const body = await req.json(); const { notification_ids, mark_all_read, archive_ids, archive_all } = body; const archiveAt = new Date().toISOString(); let didAction = false; if (archive_all) { didAction = true; const { error } = await adminClient .from('notifications') .update({ archived_at: archiveAt, is_read: true }) .eq('user_id', user.id) .is('archived_at', null); if (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } } if (archive_ids && Array.isArray(archive_ids)) { didAction = true; const { error } = await adminClient .from('notifications') .update({ archived_at: archiveAt, is_read: true }) .in('id', archive_ids) .eq('user_id', user.id); if (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } } if (mark_all_read) { didAction = true; // Mark all notifications as read const { error } = await adminClient .from('notifications') .update({ is_read: true }) .eq('user_id', user.id) .eq('is_read', false) .is('archived_at', null); if (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } if (notification_ids && Array.isArray(notification_ids)) { didAction = true; // Mark specific notifications as read const { error } = await adminClient .from('notifications') .update({ is_read: true }) .in('id', notification_ids) .eq('user_id', user.id) .is('archived_at', null); if (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } if (!didAction) { return new Response(JSON.stringify({ error: 'Invalid request body' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } // Method not allowed return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } });