d3ro-voice/server/supabase/functions/content-report/index.ts
2026-08-29 18:33:45 +09:00

94 lines
3.3 KiB
TypeScript

import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
import { requireUser, type AuthError } from '../_shared/auth.ts'
import { createServiceRoleClient } from '../_shared/quota.ts'
import {
ContentReportRequestError,
mapContentReportRpcError,
parseContentReportReceipt,
parseContentReportRequest,
parseIdempotencyKey,
} from '../_shared/content-report-contract.ts'
const MAX_REQUEST_BYTES = 25_000
function jsonResponse(body: unknown, status: number): Response {
return new Response(JSON.stringify(body), {
status,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
Pragma: 'no-cache',
},
})
}
async function readRequestJson(req: Request): Promise<unknown> {
const contentType = req.headers.get('content-type')?.toLowerCase() ?? ''
if (contentType.split(';', 1)[0].trim() !== 'application/json') {
throw new ContentReportRequestError('content_type_must_be_json', 415)
}
const declaredLength = Number(req.headers.get('content-length'))
if (Number.isFinite(declaredLength) && declaredLength > MAX_REQUEST_BYTES) {
throw new ContentReportRequestError('request_too_large', 413)
}
const rawBody = await req.text()
if (new TextEncoder().encode(rawBody).byteLength > MAX_REQUEST_BYTES) {
throw new ContentReportRequestError('request_too_large', 413)
}
try {
return JSON.parse(rawBody) as unknown
} catch {
throw new ContentReportRequestError('invalid_json')
}
}
Deno.serve(async (req: Request) => {
const preflight = handleCorsPreflightRequest(req)
if (preflight) {
const headers = new Headers(preflight.headers)
headers.set('Cache-Control', 'no-store')
return new Response(preflight.body, { status: preflight.status, headers })
}
if (req.method !== 'POST') return jsonResponse({ error: 'method_not_allowed' }, 405)
try {
const user = await requireUser(req)
const idempotencyKey = parseIdempotencyKey(req.headers.get('Idempotency-Key'))
const body = parseContentReportRequest(await readRequestJson(req))
const serviceClient = createServiceRoleClient()
const { data, error } = await serviceClient.rpc('submit_content_report_v1', {
p_actor_id: user.id,
p_idempotency_key: idempotencyKey,
p_kind: body.kind,
p_source_type: body.source.type,
p_source_id: body.source.generationId,
p_reason: body.reason,
p_comment: body.comment ?? null,
p_snapshot: body.snapshot,
})
if (error) {
const mapped = mapContentReportRpcError(error)
if (mapped.status === 500) {
console.error('content-report RPC failed', { code: error.code ?? 'unknown' })
}
return jsonResponse({ error: mapped.code }, mapped.status)
}
const receipt = parseContentReportReceipt(data)
return jsonResponse(receipt, receipt.idempotent ? 200 : 201)
} catch (error) {
if (error instanceof ContentReportRequestError) {
return jsonResponse({ error: error.code }, error.status)
}
if (error && typeof error === 'object' && 'status' in error && 'message' in error) {
const authError = error as AuthError
return jsonResponse({ error: authError.message }, authError.status)
}
console.error('content-report failed', { code: 'internal_error' })
return jsonResponse({ error: 'internal_error' }, 500)
}
})