245 lines
7.2 KiB
TypeScript
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' },
|
|
});
|
|
}
|
|
});
|