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

184 lines
5.1 KiB
TypeScript

/**
* POST /block
*
* Design intent:
* - One-tap, immediate, silent.
* - Blocking removes all visibility both ways.
* - No drama, no notification, complete separation.
*
* Flow:
* 1. Validate auth
* 2. Create block record
* 3. Remove existing follows (if any)
* 4. Log audit event
* 5. Return success
*/
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 BlockRequest {
user_id: string; // the user to block
}
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 {
// 1. Validate auth
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' },
});
}
// Handle unblock (DELETE method)
if (req.method === 'DELETE') {
const { user_id } = (await req.json()) as BlockRequest;
validateUUID(user_id, 'user_id');
const { error: deleteError } = await supabase
.from('blocks')
.delete()
.eq('blocker_id', user.id)
.eq('blocked_id', user_id);
if (deleteError) {
console.error('Error removing block:', deleteError);
return new Response(JSON.stringify({ error: 'Failed to remove block' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
const serviceClient = createServiceClient();
await serviceClient.rpc('log_audit_event', {
p_actor_id: user.id,
p_event_type: 'user_unblocked',
p_payload: { blocked_id: user_id },
});
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
// 2. Parse request (POST method)
const { user_id: blocked_id } = (await req.json()) as BlockRequest;
// 3. Validate input
validateUUID(blocked_id, 'user_id');
if (blocked_id === user.id) {
return new Response(
JSON.stringify({
error: 'Invalid block',
message: 'You cannot block yourself.',
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
// 4. Create block (idempotent - duplicate key will be ignored)
const { error: blockError } = await supabase.from('blocks').insert({
blocker_id: user.id,
blocked_id,
});
if (blockError && !blockError.message.includes('duplicate')) {
console.error('Error creating block:', blockError);
return new Response(JSON.stringify({ error: 'Failed to create block' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
// 5. Remove any existing follows (both directions)
// This ensures complete separation
const { error: unfollowError1 } = await supabase
.from('follows')
.delete()
.eq('follower_id', user.id)
.eq('following_id', blocked_id);
const { error: unfollowError2 } = await supabase
.from('follows')
.delete()
.eq('follower_id', blocked_id)
.eq('following_id', user.id);
if (unfollowError1 || unfollowError2) {
console.warn('Error removing follows during block:', unfollowError1 || unfollowError2);
// Continue anyway - block is more important
}
// 6. Log audit event
const serviceClient = createServiceClient();
await serviceClient.rpc('log_audit_event', {
p_actor_id: user.id,
p_event_type: 'user_blocked',
p_payload: { blocked_id },
});
// 7. Return success
return new Response(
JSON.stringify({
success: true,
message: 'Block applied. You will no longer see each other.',
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
if (error instanceof ValidationError) {
return new Response(
JSON.stringify({
error: 'Validation error',
message: error.message,
field: error.field,
}),
{
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' },
});
}
});