/* |-------------------------------------------------------------------------- | Agência PSI — Edge Function: recover-access (Freemium F3d) |-------------------------------------------------------------------------- | "Esqueci meu e-mail": a pessoa informa o IDENTIFICADOR (slug) do seu | ambiente. O servidor acha o e-mail do dono, dispara um magic link | (signInWithOtp — mesmo pipeline de e-mail do GoTrue) e devolve só uma DICA | MASCARADA (jo****@gm****.com). O e-mail real NUNCA volta pro cliente. |-------------------------------------------------------------------------- */ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, OPTIONS', } function json(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } // jo****@gm****.com function maskPart(s: string): string { if (!s) return '*' if (s.length <= 2) return s[0] + '***' return s.slice(0, 2) + '****' } function maskEmail(email: string): string { const [local, domain] = String(email).split('@') if (!domain) return maskPart(local) const dparts = domain.split('.') const dmasked = maskPart(dparts[0]) + (dparts.length > 1 ? '.' + dparts.slice(1).join('.') : '') return maskPart(local) + '@' + dmasked } Deno.serve(async (req: Request) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) if (req.method !== 'POST') return json({ ok: false, error: 'method_not_allowed' }, 405) try { const { slug } = await req.json().catch(() => ({})) const cleanSlug = String(slug || '').toLowerCase().trim() if (!cleanSlug || cleanSlug.length < 3) { return json({ ok: false, error: 'slug_required' }, 400) } const admin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) // slug → tenant → dono (master) → user_id const { data: tenant } = await admin.from('tenants').select('id').eq('slug', cleanSlug).maybeSingle() if (!tenant) return json({ ok: false, error: 'not_found' }) const { data: member } = await admin .from('tenant_members') .select('user_id') .eq('tenant_id', tenant.id) .eq('role', 'tenant_admin') .eq('status', 'active') .order('created_at', { ascending: true }) .limit(1) .maybeSingle() if (!member?.user_id) return json({ ok: false, error: 'not_found' }) const { data: userResp } = await admin.auth.admin.getUserById(member.user_id) const email = userResp?.user?.email if (!email) return json({ ok: false, error: 'not_found' }) // dispara o magic link via GoTrue (cliente anon — não cria usuário novo) const anon = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')! ) const { error: otpErr } = await anon.auth.signInWithOtp({ email, options: { shouldCreateUser: false }, }) if (otpErr) { console.error('[recover-access] signInWithOtp error:', otpErr.message) // ainda devolve a dica — o e-mail existe; o envio pode reprocessar } // só a DICA mascarada volta pro cliente return json({ ok: true, hint: maskEmail(email) }) } catch (err) { console.error('[recover-access] fatal:', err) return json({ ok: false, error: 'internal' }, 500) } })