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