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

182 lines
5.9 KiB
TypeScript

import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createSupabaseClient, createServiceClient } from '../_shared/supabase-client.ts'
import { trySignR2Url } from '../_shared/r2_signer.ts'
interface BeaconRequest {
lat: number
long: number
title: string
description: string
type: 'police' | 'checkpoint' | 'taskForce' | 'hazard' | 'safety' | 'community'
image_url?: string
}
interface ResponseData {
beacon?: Record<string, unknown>
error?: string
}
serve(async (req: Request) => {
try {
// Get auth header
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing Authorization header' } as ResponseData),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
const supabase = createSupabaseClient(authHeader)
const { data: { user }, error: userError } = await supabase.auth.getUser()
if (userError || !user) {
console.error('Auth error:', userError)
return new Response(
JSON.stringify({ error: 'Unauthorized' } as ResponseData),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
// Use service role for DB operations
const supabaseAdmin = createServiceClient()
// Parse request body
const body = await req.json()
// Convert lat/long to numbers (handles both int and double from client)
const beaconReq: BeaconRequest = {
lat: Number(body.lat),
long: Number(body.long),
title: body.title,
description: body.description,
type: body.type,
image_url: body.image_url
}
// Validate required fields
if (!beaconReq.lat || !beaconReq.long || !beaconReq.title || !beaconReq.description || !beaconReq.type) {
return new Response(
JSON.stringify({ error: 'Missing required fields: lat, long, title, description, type' } as ResponseData),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
// Validate beacon type
const validTypes = ['police', 'checkpoint', 'taskForce', 'hazard', 'safety', 'community']
if (!validTypes.includes(beaconReq.type)) {
return new Response(
JSON.stringify({ error: 'Invalid beacon type. Must be: police, checkpoint, taskForce, hazard, safety, or community' } as ResponseData),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
// Get user's profile and trust score (use admin client to bypass RLS)
const { data: profile, error: profileError } = await supabaseAdmin
.from('profiles')
.select('id, trust_state(harmony_score)')
.eq('id', user.id)
.single()
if (profileError || !profile) {
return new Response(
JSON.stringify({ error: 'Profile not found' } as ResponseData),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
)
}
// Get a default category for beacons (search by slug to match seed data)
const { data: category } = await supabaseAdmin
.from('categories')
.select('id')
.eq('slug', 'beacon_alerts')
.single()
let categoryId = category?.id
if (!categoryId) {
// Create the beacon category if it doesn't exist (with service role bypass)
const { data: newCategory, error: insertError } = await supabaseAdmin
.from('categories')
.insert({ slug: 'beacon_alerts', name: 'Beacon Alerts', description: 'Community safety and alert posts' })
.select('id')
.single()
if (insertError || !newCategory) {
console.error('Failed to create beacon category:', insertError)
return new Response(
JSON.stringify({ error: 'Failed to create beacon category' } as ResponseData),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
categoryId = newCategory.id
}
// Get user's trust score for initial confidence
const trustScore = profile.trust_state?.harmony_score ?? 0.5
const initialConfidence = 0.5 + (trustScore * 0.3) // Start at 50-80% based on trust
// Create the beacon post
const { data: beacon, error: beaconError } = await supabaseAdmin
.from('posts')
.insert({
author_id: user.id,
category_id: categoryId,
body: beaconReq.description,
is_beacon: true,
beacon_type: beaconReq.type,
location: `SRID=4326;POINT(${beaconReq.long} ${beaconReq.lat})`,
confidence_score: Math.min(1.0, Math.max(0.0, initialConfidence)),
is_active_beacon: true,
image_url: beaconReq.image_url,
status: 'active',
tone_label: 'neutral',
cis_score: 0.8,
allow_chain: false // Beacons don't allow chaining
})
.select()
.single()
if (beaconError) {
console.error('Error creating beacon:', beaconError)
return new Response(
JSON.stringify({ error: `Failed to create beacon: ${beaconError.message}` } as ResponseData),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
// Get full beacon data with author info
const { data: fullBeacon } = await supabaseAdmin
.from('posts')
.select(`
*,
author:profiles!posts_author_id_fkey (
id,
handle,
display_name,
avatar_url
)
`)
.eq('id', beacon.id)
.single()
let signedBeacon = fullBeacon
if (fullBeacon?.image_url) {
signedBeacon = { ...fullBeacon, image_url: await trySignR2Url(fullBeacon.image_url) }
}
return new Response(
JSON.stringify({ beacon: signedBeacon } as ResponseData),
{ status: 201, headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
console.error('Unexpected error:', error)
return new Response(
JSON.stringify({ error: 'Internal server error' } as ResponseData),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
})