251 lines
6.6 KiB
TypeScript
251 lines
6.6 KiB
TypeScript
/**
|
|
* POST /report
|
|
*
|
|
* Design intent:
|
|
* - Strict reasons only.
|
|
* - Reports never auto-remove content.
|
|
* - Reporting accuracy affects reporter trust.
|
|
*
|
|
* Flow:
|
|
* 1. Validate auth and inputs
|
|
* 2. Ensure target exists and is visible
|
|
* 3. Create report record
|
|
* 4. Log audit event
|
|
* 5. Queue for moderation review
|
|
*/
|
|
|
|
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
|
|
import { createSupabaseClient, createServiceClient } from '../_shared/supabase-client.ts';
|
|
import { validateReportReason, validateUUID, ValidationError } from '../_shared/validation.ts';
|
|
|
|
type TargetType = 'post' | 'comment' | 'profile';
|
|
|
|
interface ReportRequest {
|
|
target_type: TargetType;
|
|
target_id: string;
|
|
reason: string;
|
|
}
|
|
|
|
serve(async (req) => {
|
|
if (req.method === 'OPTIONS') {
|
|
return new Response(null, {
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'POST',
|
|
'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' },
|
|
});
|
|
}
|
|
|
|
// 2. Parse request
|
|
const { target_type, target_id, reason } = (await req.json()) as ReportRequest;
|
|
|
|
// 3. Validate inputs
|
|
if (!['post', 'comment', 'profile'].includes(target_type)) {
|
|
throw new ValidationError('Invalid target type', 'target_type');
|
|
}
|
|
|
|
validateUUID(target_id, 'target_id');
|
|
validateReportReason(reason);
|
|
|
|
// 4. Verify target exists and is visible to reporter
|
|
let targetExists = false;
|
|
let targetAuthorId: string | null = null;
|
|
|
|
if (target_type === 'post') {
|
|
const { data: post } = await supabase
|
|
.from('posts')
|
|
.select('author_id')
|
|
.eq('id', target_id)
|
|
.single();
|
|
|
|
if (post) {
|
|
targetExists = true;
|
|
targetAuthorId = post.author_id;
|
|
}
|
|
} else if (target_type === 'comment') {
|
|
const { data: comment } = await supabase
|
|
.from('comments')
|
|
.select('author_id')
|
|
.eq('id', target_id)
|
|
.single();
|
|
|
|
if (comment) {
|
|
targetExists = true;
|
|
targetAuthorId = comment.author_id;
|
|
}
|
|
} else if (target_type === 'profile') {
|
|
const { data: profile } = await supabase
|
|
.from('profiles')
|
|
.select('id')
|
|
.eq('id', target_id)
|
|
.single();
|
|
|
|
if (profile) {
|
|
targetExists = true;
|
|
targetAuthorId = target_id;
|
|
}
|
|
}
|
|
|
|
if (!targetExists) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Target not found',
|
|
message: 'The content you are trying to report does not exist or you cannot see it.',
|
|
}),
|
|
{
|
|
status: 404,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
|
|
// 5. Prevent self-reporting
|
|
if (targetAuthorId === user.id) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Invalid report',
|
|
message: 'You cannot report your own content.',
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
|
|
// 6. Check for duplicate reports (constraint will prevent, but give better message)
|
|
const { data: existingReport } = await supabase
|
|
.from('reports')
|
|
.select('id')
|
|
.eq('reporter_id', user.id)
|
|
.eq('target_type', target_type)
|
|
.eq('target_id', target_id)
|
|
.single();
|
|
|
|
if (existingReport) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Duplicate report',
|
|
message: 'You have already reported this.',
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
|
|
// 7. Create report
|
|
const { data: report, error: reportError } = await supabase
|
|
.from('reports')
|
|
.insert({
|
|
reporter_id: user.id,
|
|
target_type,
|
|
target_id,
|
|
reason,
|
|
status: 'pending',
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (reportError) {
|
|
console.error('Error creating report:', reportError);
|
|
return new Response(JSON.stringify({ error: 'Failed to create report' }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
// 8. Update reporter's trust counters
|
|
const serviceClient = createServiceClient();
|
|
await serviceClient.rpc('log_audit_event', {
|
|
p_actor_id: user.id,
|
|
p_event_type: 'report_filed',
|
|
p_payload: {
|
|
report_id: report.id,
|
|
target_type,
|
|
target_id,
|
|
target_author_id: targetAuthorId,
|
|
},
|
|
});
|
|
|
|
// Increment reports_filed counter
|
|
const { error: counterError } = await serviceClient
|
|
.from('trust_state')
|
|
.update({
|
|
counters: supabase.rpc('jsonb_set', {
|
|
target: supabase.rpc('counters'),
|
|
path: '{reports_filed}',
|
|
new_value: supabase.rpc('to_jsonb', [
|
|
supabase.rpc('jsonb_extract_path_text', ['counters', 'reports_filed']) + 1,
|
|
]),
|
|
}),
|
|
updated_at: new Date().toISOString(),
|
|
})
|
|
.eq('user_id', user.id);
|
|
|
|
if (counterError) {
|
|
console.warn('Failed to update report counter:', counterError);
|
|
// Continue anyway - report was created
|
|
}
|
|
|
|
// 9. Return success
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
report_id: report.id,
|
|
message:
|
|
'Report received. All reports are reviewed. False reports may affect your account standing.',
|
|
}),
|
|
{
|
|
status: 201,
|
|
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' },
|
|
});
|
|
}
|
|
});
|