mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.45.0'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
const PUBLIC_BASE_URL = 'https://avria.cloud'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
|
||||
|
||||
try {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const ownerIds: string[] = Array.isArray(body.owner_ids) ? body.owner_ids.filter((x: any) => typeof x === 'string') : []
|
||||
const customMessage: string = typeof body.custom_message === 'string' ? body.custom_message.trim() : ''
|
||||
|
||||
if (ownerIds.length === 0) {
|
||||
return new Response(JSON.stringify({ error: 'owner_ids required' }), {
|
||||
status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
|
||||
const serviceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
const anonKey = Deno.env.get('SUPABASE_ANON_KEY')!
|
||||
const authHeader = req.headers.get('Authorization') || ''
|
||||
|
||||
const authClient = createClient(supabaseUrl, anonKey, {
|
||||
global: { headers: { Authorization: authHeader } },
|
||||
})
|
||||
const { data: userRes } = await authClient.auth.getUser()
|
||||
if (!userRes?.user) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
const userId = userRes.user.id
|
||||
const admin = createClient(supabaseUrl, serviceKey)
|
||||
|
||||
const { data: roleRows } = await admin.from('user_roles').select('role').eq('user_id', userId)
|
||||
const roles = (roleRows || []).map((r: any) => r.role)
|
||||
if (!roles.includes('admin') && !roles.includes('manager')) {
|
||||
return new Response(JSON.stringify({ error: 'Forbidden' }), {
|
||||
status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const { data: profile } = await admin.from('profiles').select('full_name').eq('user_id', userId).maybeSingle()
|
||||
const requesterName = profile?.full_name || 'Avria Community Management'
|
||||
|
||||
const { data: owners, error: oErr } = await admin
|
||||
.from('owners')
|
||||
.select('id, first_name, last_name, email, management_contact_email, management_contact_name, business_name, owner_type, unit_id, association_id, property_address, user_id, associations(name), units(address)')
|
||||
.in('id', ownerIds)
|
||||
if (oErr) {
|
||||
return new Response(JSON.stringify({ error: 'Failed to load owners', detail: oErr.message }), {
|
||||
status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const results: Array<{ owner_id: string; ok: boolean; link?: string; error?: string; sent_to?: string }> = []
|
||||
|
||||
for (const owner of owners || []) {
|
||||
const ownerName = (owner as any).business_name
|
||||
|| (owner as any).management_contact_name
|
||||
|| [owner.first_name, owner.last_name].filter(Boolean).join(' ')
|
||||
|| 'Owner'
|
||||
const propertyAddress = (owner as any).units?.address || owner.property_address || ''
|
||||
const associationName = (owner as any).associations?.name || ''
|
||||
let recipientEmail = owner.email || (owner as any).management_contact_email || ''
|
||||
|
||||
// Fallback: if owner has a linked user_id, try profiles.email
|
||||
if (!recipientEmail && (owner as any).user_id) {
|
||||
const { data: profileRow } = await admin
|
||||
.from('profiles')
|
||||
.select('email')
|
||||
.eq('user_id', (owner as any).user_id)
|
||||
.maybeSingle()
|
||||
if (profileRow?.email) {
|
||||
recipientEmail = profileRow.email
|
||||
}
|
||||
}
|
||||
|
||||
if (!recipientEmail) {
|
||||
results.push({ owner_id: owner.id, ok: false, error: 'No email on file' })
|
||||
continue
|
||||
}
|
||||
|
||||
const { data: reqRow, error: reqErr } = await admin
|
||||
.from('tenant_info_requests')
|
||||
.insert({
|
||||
owner_id: owner.id,
|
||||
unit_id: owner.unit_id,
|
||||
association_id: owner.association_id,
|
||||
sent_to_email: recipientEmail,
|
||||
custom_message: customMessage || null,
|
||||
created_by: userId,
|
||||
})
|
||||
.select('id, token, expires_at')
|
||||
.single()
|
||||
|
||||
if (reqErr || !reqRow) {
|
||||
results.push({ owner_id: owner.id, ok: false, error: reqErr?.message || 'Failed to create request' })
|
||||
continue
|
||||
}
|
||||
|
||||
const link = `${PUBLIC_BASE_URL}/tenant-info/${reqRow.token}`
|
||||
const expiresAt = new Date(reqRow.expires_at).toLocaleDateString('en-US', {
|
||||
month: 'long', day: 'numeric', year: 'numeric',
|
||||
})
|
||||
|
||||
let emailErr: any = null
|
||||
try {
|
||||
const emailRes = await fetch(`${supabaseUrl}/functions/v1/send-transactional-email`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'apikey': anonKey,
|
||||
...(authHeader ? { Authorization: authHeader } : {}),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateName: 'tenant-info-request',
|
||||
recipientEmail,
|
||||
idempotencyKey: `tenant-info-${reqRow.id}`,
|
||||
templateData: {
|
||||
ownerName,
|
||||
propertyAddress,
|
||||
associationName,
|
||||
requesterName,
|
||||
customMessage: customMessage || undefined,
|
||||
link,
|
||||
expiresAt,
|
||||
},
|
||||
}),
|
||||
})
|
||||
if (!emailRes.ok) {
|
||||
const text = await emailRes.text()
|
||||
emailErr = { message: `status ${emailRes.status}: ${text}` }
|
||||
}
|
||||
} catch (e) {
|
||||
emailErr = e
|
||||
}
|
||||
|
||||
if (emailErr) {
|
||||
console.error('Email send failed:', emailErr)
|
||||
results.push({ owner_id: owner.id, ok: false, link, error: 'Email failed to send. Share the link manually.' })
|
||||
} else {
|
||||
results.push({ owner_id: owner.id, ok: true, link, sent_to: recipientEmail })
|
||||
}
|
||||
}
|
||||
|
||||
const sentCount = results.filter((r) => r.ok).length
|
||||
return new Response(JSON.stringify({ ok: true, sent: sentCount, total: results.length, results }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('send-tenant-info-request error:', e)
|
||||
return new Response(JSON.stringify({ error: String(e) }), {
|
||||
status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user