Add custom email verification using 6-digit OTP codes via Mailketing API: Database: - Create auth_otps table with 15-minute expiry - Add indexes and RLS policies for security - Add cleanup function for expired tokens - Insert default auth_email_verification template Edge Functions: - send-auth-otp: Generate OTP, store in DB, send via Mailketing - verify-auth-otp: Validate OTP, confirm email in Supabase Auth Frontend: - Add OTP input state to auth page - Implement send/verify OTP in useAuth hook - Add resend countdown timer (60 seconds) - Update auth flow: signup → OTP verification → login Features: - Instant email delivery (no queue/cron) - 6-digit OTP with 15-minute expiry - Resend OTP with cooldown - Admin-configurable email templates - Indonesian UI text 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
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",
|
|
};
|
|
|
|
interface VerifyOTPRequest {
|
|
user_id: string;
|
|
otp_code: string;
|
|
}
|
|
|
|
serve(async (req: Request) => {
|
|
if (req.method === "OPTIONS") {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
try {
|
|
const { user_id, otp_code }: VerifyOTPRequest = await req.json();
|
|
|
|
// Validate required fields
|
|
if (!user_id || !otp_code) {
|
|
return new Response(
|
|
JSON.stringify({ success: false, message: "Missing required fields: user_id, otp_code" }),
|
|
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Validate OTP format (6 digits)
|
|
if (!/^\d{6}$/.test(otp_code)) {
|
|
return new Response(
|
|
JSON.stringify({ success: false, message: "Invalid OTP format. Must be 6 digits." }),
|
|
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Initialize Supabase client with service role
|
|
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
|
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
|
|
console.log(`Verifying OTP for user ${user_id}`);
|
|
|
|
// Find valid OTP (not expired, not used)
|
|
const { data: otpRecord, error: otpError } = await supabase
|
|
.from('auth_otps')
|
|
.select('*')
|
|
.eq('user_id', user_id)
|
|
.eq('otp_code', otp_code)
|
|
.is('used_at', null)
|
|
.gt('expires_at', new Date().toISOString())
|
|
.order('created_at', { ascending: false })
|
|
.limit(1)
|
|
.maybeSingle();
|
|
|
|
if (otpError) {
|
|
console.error('Error fetching OTP:', otpError);
|
|
throw new Error(`Failed to verify OTP: ${otpError.message}`);
|
|
}
|
|
|
|
if (!otpRecord) {
|
|
console.log('Invalid or expired OTP');
|
|
|
|
// Clean up expired OTPs
|
|
await supabase.rpc('cleanup_expired_otps');
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
message: "Invalid or expired OTP code"
|
|
}),
|
|
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Mark OTP as used
|
|
const { error: updateError } = await supabase
|
|
.from('auth_otps')
|
|
.update({ used_at: new Date().toISOString() })
|
|
.eq('id', otpRecord.id);
|
|
|
|
if (updateError) {
|
|
console.error('Error marking OTP as used:', updateError);
|
|
throw new Error(`Failed to mark OTP as used: ${updateError.message}`);
|
|
}
|
|
|
|
// Confirm email in Supabase Auth
|
|
const { error: confirmError } = await supabase.auth.admin.updateUserById(
|
|
user_id,
|
|
{ email_confirm: true }
|
|
);
|
|
|
|
if (confirmError) {
|
|
console.error('Error confirming email:', confirmError);
|
|
throw new Error(`Failed to confirm email: ${confirmError.message}`);
|
|
}
|
|
|
|
console.log(`Email confirmed successfully for user ${user_id}`);
|
|
|
|
// Clean up expired OTPs
|
|
await supabase.rpc('cleanup_expired_otps');
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: "Email verified successfully"
|
|
}),
|
|
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
|
|
} catch (error: any) {
|
|
console.error("Error verifying OTP:", error);
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
message: error.message || "Failed to verify OTP"
|
|
}),
|
|
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
});
|