182 lines
5.9 KiB
TypeScript
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' } }
|
|
)
|
|
}
|
|
})
|