/** * Auth0 Webhook Edge Function * * Handles Auth0 webhook events for real-time sync */ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { createHmac } from 'https://deno.land/std@0.168.0/node/crypto.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-auth0-signature', }; /** * Verify Auth0 webhook signature */ function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean { const hmac = createHmac('sha256', secret); hmac.update(payload); const expectedSignature = hmac.digest('hex'); return signature === expectedSignature; } serve(async (req) => { // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } try { // Get webhook secret const WEBHOOK_SECRET = Deno.env.get('AUTH0_WEBHOOK_SECRET'); if (!WEBHOOK_SECRET) { throw new Error('Webhook secret not configured'); } // Verify signature const signature = req.headers.get('x-auth0-signature'); const body = await req.text(); if (signature && !verifyWebhookSignature(body, signature, WEBHOOK_SECRET)) { return new Response( JSON.stringify({ error: 'Invalid signature' }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401, } ); } const event = JSON.parse(body); const { type, data } = event; // Create Supabase admin client const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); // Handle different event types switch (type) { case 'post-login': { // Update last login timestamp const auth0Sub = data.user?.user_id; if (auth0Sub) { await supabase .from('profiles') .update({ updated_at: new Date().toISOString() }) .eq('auth0_sub', auth0Sub); // Log to audit log await supabase.rpc('log_admin_action', { p_user_id: null, p_action: 'auth0_login', p_details: { auth0_sub: auth0Sub, event_type: 'post-login' }, }); } break; } case 'post-change-password': { // Log password change const auth0Sub = data.user?.user_id; if (auth0Sub) { await supabase.rpc('log_admin_action', { p_user_id: null, p_action: 'password_changed', p_details: { auth0_sub: auth0Sub, event_type: 'post-change-password' }, }); } break; } case 'post-user-registration': { // Create initial profile if doesn't exist const auth0Sub = data.user?.user_id; const email = data.user?.email; if (auth0Sub && email) { const { data: existing } = await supabase .from('profiles') .select('id') .eq('auth0_sub', auth0Sub) .single(); if (!existing) { const username = email.split('@')[0] + '_' + Math.random().toString(36).substring(7); await supabase .from('profiles') .insert({ auth0_sub: auth0Sub, email: email, username: username, display_name: data.user?.name || username, }); } } break; } default: console.log('[Auth0Webhook] Unhandled event type:', type); } // Log webhook event await supabase .from('auth0_sync_log') .insert({ auth0_sub: data.user?.user_id || 'unknown', sync_status: 'success', user_data: { event_type: type, data }, }); return new Response( JSON.stringify({ success: true }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, } ); } catch (error) { console.error('[Auth0Webhook] Error:', error); return new Response( JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500, } ); } });