sojorn/_legacy/supabase/functions/notifications/index.ts
2026-02-15 00:33:24 -06:00

245 lines
7.2 KiB
TypeScript

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<string, unknown>;
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' },
});
}
});