/** * POST /save - Save a post (private bookmark) * DELETE /save - Remove from saved * * Design intent: * - Saves are private, for personal curation * - More intentional than appreciation * - Saves > likes in ranking algorithm */ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createSupabaseClient, createServiceClient } from '../_shared/supabase-client.ts'; import { validateUUID, ValidationError } from '../_shared/validation.ts'; interface SaveRequest { post_id: string; } serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, DELETE', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }, }); } try { const authHeader = req.headers.get('Authorization'); if (!authHeader) { return new Response(JSON.stringify({ error: 'Missing authorization header' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } const supabase = createSupabaseClient(authHeader); const { data: { user }, error: authError, } = await supabase.auth.getUser(); if (authError || !user) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } const { post_id } = (await req.json()) as SaveRequest; validateUUID(post_id, 'post_id'); // Use admin client to bypass RLS issues const adminClient = createServiceClient(); // Verify post exists and check visibility const { data: postRow, error: postError } = await adminClient .from('posts') .select('id, visibility, author_id, status') .eq('id', post_id) .maybeSingle(); if (postError || !postRow) { console.error('Post lookup failed:', { post_id, error: postError?.message }); return new Response( JSON.stringify({ error: 'Post not found' }), { status: 404, headers: { 'Content-Type': 'application/json' } } ); } // Check if post is active if (postRow.status !== 'active') { return new Response( JSON.stringify({ error: 'Post is not available' }), { status: 404, headers: { 'Content-Type': 'application/json' } } ); } // For private posts, verify the user has access (must be author) if (postRow.visibility === 'private' && postRow.author_id !== user.id) { return new Response( JSON.stringify({ error: 'Post not accessible' }), { status: 403, headers: { 'Content-Type': 'application/json' } } ); } // For followers-only posts, verify the user follows the author if (postRow.visibility === 'followers' && postRow.author_id !== user.id) { const { data: followRow } = await adminClient .from('follows') .select('status') .eq('follower_id', user.id) .eq('following_id', postRow.author_id) .eq('status', 'accepted') .maybeSingle(); if (!followRow) { return new Response( JSON.stringify({ error: 'You must follow this user to save their posts' }), { status: 403, headers: { 'Content-Type': 'application/json' } } ); } } // Handle unsave (DELETE) if (req.method === 'DELETE') { const { error: deleteError } = await adminClient .from('post_saves') .delete() .eq('user_id', user.id) .eq('post_id', post_id); if (deleteError) { console.error('Error removing save:', deleteError); return new Response(JSON.stringify({ error: 'Failed to remove from saved' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } // Handle save (POST) const { error: saveError } = await adminClient .from('post_saves') .insert({ user_id: user.id, post_id, }); if (saveError) { // Already saved (duplicate key) if (saveError.code === '23505') { return new Response( JSON.stringify({ error: 'Post already saved' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } console.error('Error saving post:', saveError); return new Response(JSON.stringify({ error: 'Failed to save post' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } return new Response( JSON.stringify({ success: true, message: 'Saved. You can find this in your collection.', }), { status: 200, headers: { 'Content-Type': 'application/json' }, } ); } catch (error) { if (error instanceof ValidationError) { return new Response( JSON.stringify({ error: 'Validation error', message: error.message }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } console.error('Unexpected error:', error); return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } });