UNPKG

gebeya-telegram-otp

Version:

Reusable Telegram phone verification components for React applications

1,411 lines (1,359 loc) 49.3 kB
# Telegram Verification Setup Guide This guide walks you through setting up the Telegram verification system in your Lovable application. ## Prerequisites - Lovable project with Supabase integration - Telegram account - Basic knowledge of React and Supabase ## Step 1: Create Telegram Bot 1. Open Telegram and search for [@BotFather](https://t.me/botfather) 2. Start a chat and use `/newbot` command 3. Follow the instructions to create your bot 4. Save the bot token - you'll need it later 5. Note your bot username (e.g., `@your_bot_name`) ## Step 2: Database Setup In your Supabase SQL Editor, run: ```sql -- Create verification sessions table CREATE TABLE public.verification_sessions ( id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, phone_number TEXT NOT NULL, telegram_user_id BIGINT, otp_code TEXT, verified BOOLEAN NOT NULL DEFAULT false, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() ); -- Enable Row Level Security ALTER TABLE public.verification_sessions ENABLE ROW LEVEL SECURITY; -- Create policies for verification process CREATE POLICY "Anyone can insert verification sessions" ON public.verification_sessions FOR INSERT WITH CHECK (true); CREATE POLICY "Anyone can read verification sessions for verification process" ON public.verification_sessions FOR SELECT USING (true); CREATE POLICY "Anyone can update verification sessions" ON public.verification_sessions FOR UPDATE USING (true); ``` ## Step 3: Set Environment Variables In Supabase Dashboard > Settings > Edge Functions, add: - `TELEGRAM_BOT_TOKEN`: Your bot token from Step 1 ## Step 4: Deploy Edge Functions Create these edge functions in your `supabase/functions/` directory: ### Add the folloing in your `supabase/config.toml/` [functions.telegram-webhook] verify_jwt = false [functions.verify-otp] verify_jwt = false ### Telegram Webhook (`supabase/functions/telegram-webhook/index.ts`) ```typescript import { serve } from "https://deno.land/std@0.168.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", }; // OTP configuration const OTP_CONFIG = { MAX_ATTEMPTS_PER_WINDOW: 3, TIME_WINDOW_MINUTES: 5, OTP_EXPIRY_MINUTES: 5, // OTP expiry time in minutes }; serve(async (req) => { console.log("=== TELEGRAM WEBHOOK CALLED ==="); console.log("Request method:", req.method); console.log("Request URL:", req.url); console.log("Request headers:", Object.fromEntries(req.headers.entries())); if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders, }); } try { const requestBody = await req.json(); console.log("Request body received:", JSON.stringify(requestBody, null, 2)); // Handle webhook setup request if (requestBody.action === "setup_webhook") { console.log("Setting up Telegram webhook..."); const botToken = Deno.env.get("TELEGRAM_BOT_TOKEN"); const supabaseUrl = Deno.env.get("SUPABASE_URL"); if (!botToken) { return new Response( JSON.stringify({ error: "TELEGRAM_BOT_TOKEN not configured", success: false, }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } if (!supabaseUrl) { return new Response( JSON.stringify({ error: "SUPABASE_URL not configured", success: false, }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } const webhookUrl = `${supabaseUrl}/functions/v1/telegram-webhook`; // Set up the webhook const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`; const setupResponse = await fetch(telegramApiUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: webhookUrl, allowed_updates: [ "message", "my_chat_member", "inline_query", "callback_query", ], }), }); const setupResult = await setupResponse.json(); console.log("Webhook setup result:", setupResult); if (setupResult.ok) { // Verify the webhook was set correctly const verifyUrl = `https://api.telegram.org/bot${botToken}/getWebhookInfo`; const verifyResponse = await fetch(verifyUrl); const webhookInfo = await verifyResponse.json(); return new Response( JSON.stringify({ success: true, message: "Webhook setup successfully", webhook_url: webhookUrl, webhook_info: webhookInfo.result, }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } else { return new Response( JSON.stringify({ success: false, error: "Failed to setup webhook", details: setupResult, }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } } // Handle webhook verification request if (requestBody.action === "verify_webhook") { console.log("Verifying Telegram webhook..."); const botToken = Deno.env.get("TELEGRAM_BOT_TOKEN"); if (!botToken) { return new Response( JSON.stringify({ error: "TELEGRAM_BOT_TOKEN not configured", success: false, }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } const verifyUrl = `https://api.telegram.org/bot${botToken}/getWebhookInfo`; const verifyResponse = await fetch(verifyUrl); const webhookInfo = await verifyResponse.json(); return new Response( JSON.stringify({ success: true, webhook_info: webhookInfo.result, }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } // Check if this is a request to create a verification session (from web app) if ( requestBody.phone_number && !requestBody.message && !requestBody.my_chat_member ) { console.log( "Creating verification session for phone:", requestBody.phone_number ); const supabaseUrl = Deno.env.get("SUPABASE_URL"); const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!supabaseUrl || !supabaseKey) { console.error("Supabase environment variables not found"); return new Response( JSON.stringify({ error: "Supabase not configured", }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } const supabase = createClient(supabaseUrl, supabaseKey, { auth: { autoRefreshToken: false, persistSession: false, }, }); // Create a verification session const { data: session, error: sessionError } = await supabase .from("verification_sessions") .insert({ phone_number: requestBody.phone_number, expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(), // 10 minutes }) .select() .single(); if (sessionError) { console.error("Error creating verification session:", sessionError); return new Response( JSON.stringify({ error: "Failed to create verification session", }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } console.log("Verification session created:", session.id); return new Response( JSON.stringify({ session_id: session.id, }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } // This is a Telegram webhook update const update = requestBody; console.log("Telegram update received:", JSON.stringify(update, null, 2)); const botToken = Deno.env.get("TELEGRAM_BOT_TOKEN"); if (!botToken) { console.error("TELEGRAM_BOT_TOKEN not found"); return new Response("Bot token not configured", { status: 500, }); } const supabaseUrl = Deno.env.get("SUPABASE_URL"); const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!supabaseUrl || !supabaseKey) { console.error("Supabase environment variables not found"); return new Response("Supabase not configured", { status: 500, }); } const supabase = createClient(supabaseUrl, supabaseKey, { auth: { autoRefreshToken: false, persistSession: false, }, }); // Helper function to normalize phone numbers for comparison const normalizePhone = (phone) => { // Remove all non-digit characters and normalize let normalized = phone.replace(/[\s\-\(\)]/g, ""); // Remove leading + if present if (normalized.startsWith("+")) { normalized = normalized.substring(1); } return normalized; }; // Helper function to send Telegram messages const sendTelegramMessage = async (chatId, text, replyMarkup) => { const telegramApiUrl = `https://api.telegram.org/bot${botToken}/sendMessage`; const payload = { chat_id: chatId, text: text, parse_mode: "HTML", }; if (replyMarkup) { payload.reply_markup = replyMarkup; } const response = await fetch(telegramApiUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { console.error( "Failed to send Telegram message:", await response.text() ); } return response; }; // Function to check OTP rate limits const checkOtpRateLimit = async (phone, userId) => { const timeWindow = new Date(); timeWindow.setMinutes( timeWindow.getMinutes() - OTP_CONFIG.TIME_WINDOW_MINUTES ); // Query for recent OTPs for this phone or user const query = supabase .from("verification_sessions") .select("*") .gte("created_at", timeWindow.toISOString()); // Filter by phone if available, or by user ID if available let filter; if (phone) { const normalizedPhone = normalizePhone(phone); filter = query.filter("phone_number", "ilike", `%${normalizedPhone}%`); } else if (userId) { filter = query.eq("telegram_user_id", userId); } else { // If neither is available, we can't check rate limits return { allowed: true, }; } const { data: recentOtps, error } = await filter; if (error) { console.error("Error checking OTP rate limits:", error); // If there's an error, allow the operation to proceed as a fallback return { allowed: true, }; } // Count valid attempts in the time window const validAttempts = recentOtps ? recentOtps.length : 0; console.log( `Rate limit check: ${validAttempts}/${OTP_CONFIG.MAX_ATTEMPTS_PER_WINDOW} attempts in the last ${OTP_CONFIG.TIME_WINDOW_MINUTES} minutes` ); return { allowed: validAttempts < OTP_CONFIG.MAX_ATTEMPTS_PER_WINDOW, attemptsUsed: validAttempts, attemptsRemaining: OTP_CONFIG.MAX_ATTEMPTS_PER_WINDOW - validAttempts, nextResetTime: timeWindow.getTime() + OTP_CONFIG.TIME_WINDOW_MINUTES * 60 * 1000, }; }; // Helper function to check if user is verified for a specific phone number const isUserVerifiedForPhone = async (userId, phoneNumber) => { console.log( "Checking if user is verified for specific phone:", userId, "phone:", phoneNumber ); const { data: existingUsers, error: userCheckError } = await supabase.auth.admin.listUsers(); if (userCheckError) { console.error("Error checking existing users:", userCheckError); return false; } if (!existingUsers?.users) { return false; } // If no specific phone number provided, just check if user is verified with any phone if (!phoneNumber) { const verifiedUser = existingUsers.users.find( (user) => user.user_metadata?.telegram_user_id === userId && user.phone_confirmed_at !== null ); return !!verifiedUser; } // Check if user is verified for this specific phone number const normalizedTargetPhone = normalizePhone(phoneNumber); const verifiedUser = existingUsers.users.find( (user) => user.user_metadata?.telegram_user_id === userId && user.phone_confirmed_at !== null && user.phone && normalizePhone(user.phone) === normalizedTargetPhone ); console.log( "User verification status for specific phone:", !!verifiedUser, "normalized phone:", normalizedTargetPhone ); return !!verifiedUser; }; // Helper function to handle automatic welcome const handleAutomaticWelcome = async (chatId, userId, expectedPhone) => { console.log( "Handling automatic welcome for user:", userId, "expected phone:", expectedPhone ); // First, check if this user already has a verified phone number in our system const { data: existingUsers, error: userCheckError } = await supabase.auth.admin.listUsers(); let userHasPhone = false; let userPhone = null; if (!userCheckError && existingUsers?.users) { const user = existingUsers.users.find( (u) => u.user_metadata?.telegram_user_id === userId ); if (user && user.phone) { userHasPhone = true; userPhone = user.phone; console.log("User already has phone in our system:", userPhone); } } // Check if there's an existing verification session for this chat_id console.log( "Checking for existing verification session for chat_id:", chatId ); const { data: existingSession, error: sessionError } = await supabase .from("verification_sessions") .select("*") .eq("telegram_chat_id", chatId) .gte("expires_at", new Date().toISOString()) .order("created_at", { ascending: false, }) .limit(1) .maybeSingle(); if (sessionError) { console.error("Error checking existing session:", sessionError); } // If we have an expected phone (from the URL) and the user already has a phone // OR if we have an existing session with OTP if (existingSession && existingSession.otp_code) { // Session already has OTP, user needs to enter it on website await sendTelegramMessage( chatId, `🔐 <b>Verification In Progress</b>\n\nYour OTP code is: <code>${existingSession.otp_code}</code>\n\nPlease return to the website and enter this code to complete your verification.\n\n This code will expire in ${OTP_CONFIG.OTP_EXPIRY_MINUTES} minutes.` ); return; } // If user already has a phone number stored, generate OTP directly without asking to share if (userHasPhone || (expectedPhone && existingSession?.phone_number)) { // Check rate limits before proceeding const phoneToUse = userPhone || expectedPhone || existingSession?.phone_number; const rateLimitCheck = await checkOtpRateLimit(phoneToUse, userId); if (!rateLimitCheck.allowed) { const resetTime = new Date(rateLimitCheck.nextResetTime); const minutesUntilReset = Math.ceil( (resetTime - new Date()) / (60 * 1000) ); await sendTelegramMessage( chatId, `⚠️ <b>Rate Limit Reached</b>\n\nYou've requested too many verification codes. Please try again in ${minutesUntilReset} minutes.` ); return; } // Generate OTP const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); console.log( "Generated OTP for existing user without sharing phone:", otpCode ); // Create or update session let session; let sessionError; if (existingSession) { // Update existing session const { data: updatedSession, error: updateError } = await supabase .from("verification_sessions") .update({ telegram_user_id: userId, telegram_chat_id: chatId, phone_number: phoneToUse, otp_code: otpCode, expires_at: new Date( Date.now() + OTP_CONFIG.OTP_EXPIRY_MINUTES * 60 * 1000 ).toISOString(), }) .eq("id", existingSession.id) .select() .single(); session = updatedSession; sessionError = updateError; } else { // Create new session const { data: newSession, error: insertError } = await supabase .from("verification_sessions") .insert({ phone_number: phoneToUse, telegram_user_id: userId, telegram_chat_id: chatId, otp_code: otpCode, expires_at: new Date( Date.now() + OTP_CONFIG.OTP_EXPIRY_MINUTES * 60 * 1000 ).toISOString(), }) .select() .single(); session = newSession; sessionError = insertError; } if (sessionError) { console.error( "Error creating/updating verification session:", sessionError ); await sendTelegramMessage( chatId, "❌ <b>Error</b>\n\nSomething went wrong. Please try again later." ); } else { console.log("Verification session created/updated:", session.id); await sendTelegramMessage( chatId, ` <b>Verification Code</b>\n\nYour OTP code is: <code>${otpCode}</code>\n\nThis code will expire in ${OTP_CONFIG.OTP_EXPIRY_MINUTES} minutes. Return to the website and enter this code to complete your verification.`, { remove_keyboard: true, } ); } return; } // If user doesn't have a stored phone number, prompt them to share if (existingSession && !existingSession.otp_code) { // Session exists but no OTP yet, waiting for contact sharing let message = "📱 <b>Continue Verification</b>\n\n"; if (expectedPhone) { message += `Please share the phone number <b>${expectedPhone}</b> by clicking the button below.\n\n⚠️ <b>Important:</b> You must share the exact same phone number you entered on the website.`; } else if (existingSession.phone_number) { message += `Please share your phone number <b>${existingSession.phone_number}</b> by clicking the button below to continue verification.`; } else { message += "To complete your account setup, please share your phone number by clicking the button below."; } await sendTelegramMessage(chatId, message, { keyboard: [ [ { text: "📱 Share Phone Number", request_contact: true, }, ], ], resize_keyboard: true, one_time_keyboard: true, }); } else { // No existing session for this chat_id, proceed with normal flow console.log("No existing session found for chat_id:", chatId); // User is not verified for the expected phone, ask for contact let message = "🔐 <b>Phone Verification Required</b>\n\n"; if (expectedPhone) { message += `Please share the phone number <b>${expectedPhone}</b> by clicking the button below.\n\n⚠️ <b>Important:</b> You must share the exact same phone number you entered on the website.`; } else { message += "To complete your account setup, please share your phone number by clicking the button below."; } await sendTelegramMessage(chatId, message, { keyboard: [ [ { text: "📱 Share Phone Number", request_contact: true, }, ], ], resize_keyboard: true, one_time_keyboard: true, }); } }; // Extract expected phone number from start parameter const extractExpectedPhone = (text) => { if (text && text.includes("/start verify_")) { const parts = text.split("verify_"); if (parts.length > 1) { let phoneNumber = parts[1].trim(); console.log("Extracted phone from start command:", phoneNumber); // Handle both encoded and direct phone numbers try { // First try to decode if it appears to be URI encoded if (phoneNumber.includes("%")) { phoneNumber = decodeURIComponent(phoneNumber); } // Convert raw digits back to international format // If it's just digits, add the + prefix if (/^\d+$/.test(phoneNumber)) { phoneNumber = "+" + phoneNumber; } console.log("Processed expected phone:", phoneNumber); return phoneNumber; } catch (error) { console.error("Error processing phone number:", error); // Fallback: just add + if it's all digits if (/^\d+$/.test(phoneNumber)) { return "+" + phoneNumber; } return phoneNumber; } } } return null; }; // Handle different types of updates if (update.message) { const message = update.message; const chatId = message.chat.id; const userId = message.from.id; const text = message.text; console.log( "Processing message from user:", userId, "chat:", chatId, "text:", text ); // Extract expected phone number from start command const expectedPhone = extractExpectedPhone(text); console.log("Expected phone number from start command:", expectedPhone); // Handle contact sharing first if (message.contact) { console.log("Contact shared:", message.contact); const sharedPhone = message.contact.phone_number.startsWith("+") ? message.contact.phone_number : "+" + message.contact.phone_number; console.log("Processing contact verification for phone:", sharedPhone); // Check rate limits before proceeding const rateLimitCheck = await checkOtpRateLimit(sharedPhone, userId); if (!rateLimitCheck.allowed) { const resetTime = new Date(rateLimitCheck.nextResetTime); const minutesUntilReset = Math.ceil( (resetTime - new Date()) / (60 * 1000) ); await sendTelegramMessage( chatId, `⚠️ <b>Rate Limit Reached</b>\n\nYou've requested too many verification codes. Please try again in ${minutesUntilReset} minutes.` ); return new Response("OK", { status: 200, headers: corsHeaders, }); } // If we have an expected phone number, validate it matches the shared contact if (expectedPhone) { const normalizedExpected = normalizePhone(expectedPhone); const normalizedShared = normalizePhone(sharedPhone); console.log( "Comparing phones - Expected:", normalizedExpected, "Shared:", normalizedShared ); if (normalizedExpected !== normalizedShared) { await sendTelegramMessage( chatId, ` <b>Phone Number Mismatch</b>\n\nYou shared: <code>${sharedPhone}</code>\nExpected: <code>${expectedPhone}</code>\n\nPlease share the exact same phone number you entered on the website.`, { keyboard: [ [ { text: "📱 Share Correct Phone Number", request_contact: true, }, ], ], resize_keyboard: true, one_time_keyboard: true, } ); return new Response("OK", { status: 200, headers: corsHeaders, }); } } // Generate OTP - Always generate a new OTP regardless of user verification status const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); console.log("Generated OTP:", otpCode); // Find existing verification session or create new one // Try to find by exact phone match first let existingSessions = []; let findError = null; try { const { data, error } = await supabase .from("verification_sessions") .select("*") .eq("phone_number", sharedPhone) .gte("expires_at", new Date().toISOString()) .order("created_at", { ascending: false, }) .limit(1); existingSessions = data || []; findError = error; // If no exact match found, try to find by all sessions and check normalized phones if (existingSessions.length === 0) { const { data: allSessions, error: allError } = await supabase .from("verification_sessions") .select("*") .gte("expires_at", new Date().toISOString()) .order("created_at", { ascending: false, }); if (allSessions && !allError) { const normalizedShared = normalizePhone(sharedPhone); existingSessions = allSessions .filter( (session) => normalizePhone(session.phone_number) === normalizedShared ) .slice(0, 1); } } } catch (error) { console.error("Error finding existing sessions:", error); findError = error; } let session; let sessionError; if (findError) { console.error("Error finding existing sessions:", findError); } if (existingSessions && existingSessions.length > 0) { // Update existing session with OTP and telegram_user_id console.log("Updating existing session:", existingSessions[0].id); const { data: updatedSession, error: updateError } = await supabase .from("verification_sessions") .update({ telegram_user_id: userId, telegram_chat_id: chatId, otp_code: otpCode, expires_at: new Date( Date.now() + OTP_CONFIG.OTP_EXPIRY_MINUTES * 60 * 1000 ).toISOString(), }) .eq("id", existingSessions[0].id) .select() .single(); session = updatedSession; sessionError = updateError; } else { // Create new verification session if none exists console.log("Creating new verification session"); const { data: newSession, error: insertError } = await supabase .from("verification_sessions") .insert({ phone_number: sharedPhone, telegram_user_id: userId, telegram_chat_id: chatId, otp_code: otpCode, expires_at: new Date( Date.now() + OTP_CONFIG.OTP_EXPIRY_MINUTES * 60 * 1000 ).toISOString(), }) .select() .single(); session = newSession; sessionError = insertError; } if (sessionError) { console.error("Error creating verification session:", sessionError); await sendTelegramMessage( chatId, "❌ <b>Error</b>\n\nSomething went wrong. Please try again later." ); } else { console.log("Verification session created:", session.id); await sendTelegramMessage( chatId, ` <b>Contact Verified!</b>\n\nYour OTP code is: <code>${otpCode}</code>\n\nThis code will expire in ${OTP_CONFIG.OTP_EXPIRY_MINUTES} minutes. Return to the website and enter this code to complete your verification.`, { remove_keyboard: true, } ); } } else { console.log("Triggering automatic welcome for any message"); await handleAutomaticWelcome(chatId, userId, expectedPhone); } } // Handle when user joins/visits the chat (my_chat_member updates) if (update.my_chat_member) { const chatMember = update.my_chat_member; const chatId = chatMember.chat.id; const userId = chatMember.from.id; console.log("Chat member update:", chatMember.new_chat_member.status); // If bot was added or user started the chat, send automatic welcome if ( chatMember.new_chat_member.status === "member" || chatMember.new_chat_member.status === "administrator" ) { console.log( "Bot added to chat or user started chat, sending automatic welcome" ); await handleAutomaticWelcome(chatId, userId); } } // Handle when new members join the chat if (update.message && update.message.new_chat_members) { const message = update.message; const chatId = message.chat.id; const userId = message.from.id; console.log("New chat members detected, sending automatic welcome"); await handleAutomaticWelcome(chatId, userId); } // Handle inline queries (when user is redirected from web app) if (update.inline_query) { const inlineQuery = update.inline_query; const userId = inlineQuery.from.id; const chatId = userId; // For inline queries, we use the user ID as chat ID console.log("Inline query received, triggering automatic welcome"); await handleAutomaticWelcome(chatId, userId); } // Handle callback queries (button presses) if (update.callback_query) { const callbackQuery = update.callback_query; const chatId = callbackQuery.message?.chat.id; const userId = callbackQuery.from.id; if (chatId) { console.log("Callback query received, triggering automatic welcome"); await handleAutomaticWelcome(chatId, userId); } } return new Response("OK", { status: 200, headers: corsHeaders, }); } catch (error) { console.error("Webhook error:", error); return new Response("Internal Server Error", { status: 500, headers: corsHeaders, }); } }); ``` ### OTP Verification (`supabase/functions/verify-otp/index.ts`) ```typescript import { serve } from "https://deno.land/std@0.168.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", }; // Function to send success message to Telegram async function sendTelegramSuccessMessage(chatId) { const botToken = Deno.env.get("TELEGRAM_BOT_TOKEN"); if (!botToken) { console.error("TELEGRAM_BOT_TOKEN not found in environment variables"); return; } const message = `🎉 <b>Verification Successful!</b> Your phone number has been verified successfully. 🔐 Your account is now secured and ready to use. You can now close this chat and return to the application to continue. Thank you for using our secure verification system!`; try { const response = await fetch( `https://api.telegram.org/bot${botToken}/sendMessage`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ chat_id: chatId, text: message, parse_mode: "HTML", }), } ); if (!response.ok) { const errorData = await response.text(); console.error("Telegram API error:", response.status, errorData); } else { console.log("Success message sent to Telegram chat:", chatId); } } catch (error) { console.error("Error sending Telegram message:", error); } } serve(async (req) => { console.log("=== VERIFY-OTP FUNCTION CALLED ==="); console.log("Request method:", req.method); // Handle CORS preflight requests if (req.method === "OPTIONS") { console.log("Handling CORS preflight request"); return new Response(null, { headers: corsHeaders, }); } try { const body = await req.json(); console.log("Request body received:", JSON.stringify(body, null, 2)); const { phone_number, otp_code, redirect_url } = body; // Default redirect URL if not provided const defaultRedirectUrl = "/dashboard"; const finalRedirectUrl = redirect_url || defaultRedirectUrl; if (!phone_number || !otp_code) { console.log( "Missing required fields - phone_number:", !!phone_number, "otp_code:", !!otp_code ); return new Response( JSON.stringify({ error: "Phone number and OTP are required", }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } console.log( "Validating OTP for phone:", phone_number, "with code:", otp_code ); const supabaseUrl = Deno.env.get("SUPABASE_URL"); const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!supabaseUrl || !supabaseKey) { console.error("Missing Supabase environment variables"); return new Response( JSON.stringify({ error: "Server configuration error", }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } // Use service role key for admin operations const supabase = createClient(supabaseUrl, supabaseKey, { auth: { autoRefreshToken: false, persistSession: false, }, }); // Helper function to normalize phone numbers for comparison const normalizePhone = (phone) => { // Remove all non-digit characters and normalize let normalized = phone.replace(/[\s\-\(\)]/g, ""); // Remove leading + if present if (normalized.startsWith("+")) { normalized = normalized.substring(1); } return normalized; }; // Check if OTP is valid and not expired console.log("Querying verification_sessions table for:", { phone_number, otp_code, }); const { data: sessions, error: queryError } = await supabase .from("verification_sessions") .select("*") .eq("otp_code", otp_code) .eq("verified", false) .gt("expires_at", new Date().toISOString()) .order("created_at", { ascending: false, }); console.log("Database query results:"); console.log("- Sessions found:", sessions?.length || 0); console.log("- Query error:", queryError); if (queryError) { console.error("Database query error:", queryError); return new Response( JSON.stringify({ error: "Database error", details: queryError.message, }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } if (!sessions || sessions.length === 0) { console.log("No valid sessions found with OTP:", otp_code); return new Response( JSON.stringify({ error: "Invalid or expired OTP", }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } // Find a session that matches the phone number (with flexible matching) const normalizedInputPhone = normalizePhone(phone_number); console.log("Normalized input phone:", normalizedInputPhone); let matchingSession = null; for (const session of sessions) { const normalizedSessionPhone = normalizePhone(session.phone_number); console.log( "Comparing with session phone:", session.phone_number, "normalized:", normalizedSessionPhone ); if (normalizedInputPhone === normalizedSessionPhone) { matchingSession = session; break; } } if (!matchingSession) { console.log("No session found with matching phone number"); console.log( "Available sessions:", sessions.map((s) => ({ id: s.id, phone: s.phone_number, })) ); // Get the actual Telegram phone number for better error message const telegramPhone = sessions.length > 0 ? sessions[0].phone_number : "unknown"; return new Response( JSON.stringify({ error: `Phone number mismatch. You entered ${phone_number} but your Telegram account shows ${telegramPhone}. Please enter the same phone number that is registered with your Telegram account.`, entered_phone: phone_number, telegram_phone: telegramPhone, }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } console.log("Using matching session:", matchingSession.id); // Check if user already exists with this phone number console.log("Checking if user already exists with phone:", phone_number); const { data: existingUsers, error: userCheckError } = await supabase.auth.admin.listUsers(); if (userCheckError) { console.error("Error checking existing users:", userCheckError); } let user = null; if (existingUsers?.users) { user = existingUsers.users.find((u) => { if (u.phone) { return normalizePhone(u.phone) === normalizedInputPhone; } return false; }); console.log("Existing user found:", !!user); } // Create user if doesn't exist if (!user) { console.log("Creating new user with phone:", phone_number); const { data: newUser, error: createError } = await supabase.auth.admin.createUser({ phone: phone_number, phone_confirm: true, user_metadata: { phone_verified: true, verification_method: "telegram_otp", telegram_user_id: matchingSession.telegram_user_id, telegram_chat_id: matchingSession.telegram_chat_id, }, }); if (createError) { console.error("Error creating user:", createError); return new Response( JSON.stringify({ error: "Failed to create user", details: createError.message, }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } user = newUser.user; console.log("User created successfully:", user?.id); } else { console.log("User already exists, updating verification status"); const { data: updatedUser, error: updateError } = await supabase.auth.admin.updateUserById(user.id, { phone_confirm: true, user_metadata: { ...user.user_metadata, phone_verified: true, verification_method: "telegram_otp", telegram_user_id: matchingSession.telegram_user_id, telegram_chat_id: matchingSession.telegram_chat_id, }, }); if (updateError) { console.error("Error updating user:", updateError); } else { user = updatedUser.user; console.log("User updated successfully"); } } // Mark verification session as verified console.log("Marking session as verified..."); const { error: updateError } = await supabase .from("verification_sessions") .update({ verified: true, }) .eq("id", matchingSession.id); if (updateError) { console.error("Error updating verification status:", updateError); return new Response( JSON.stringify({ error: "Verification failed", details: updateError.message, }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } // Send success message to Telegram if (matchingSession.telegram_chat_id) { console.log( "Sending success message to Telegram chat:", matchingSession.telegram_chat_id ); await sendTelegramSuccessMessage(matchingSession.telegram_chat_id); } // Generate session for the user console.log("Generating session for user:", user?.id); // Get the anon key to create an auth client const supabaseAnonKey = Deno.env.get("SUPABASE_ANON_KEY"); if (!supabaseAnonKey) { console.error("Missing SUPABASE_ANON_KEY environment variable"); return new Response( JSON.stringify({ error: "Server configuration error", }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } // Create a magic link for the user let sessionData = null; try { // Create an auth link that will automatically sign in the user const { data, error: linkError } = await supabase.auth.admin.generateLink( { type: "magiclink", email: `${user.id}@temp-auth.com`, options: { redirectTo: finalRedirectUrl, }, } ); if (linkError) { console.error("Error generating auth link:", linkError); } else { console.log("Auth link generated successfully"); sessionData = { properties: data, }; } } catch (error) { console.error("Error generating auth link:", error); } console.log("=== OTP VERIFICATION SUCCESSFUL ==="); console.log("Session ID:", matchingSession.id); console.log("Phone:", phone_number); console.log("User ID:", user?.id); console.log("Auth session created successfully"); return new Response( JSON.stringify({ success: true, message: "Phone number verified successfully", session_id: matchingSession.id, user: { id: user?.id, phone: user?.phone, phone_confirmed_at: user?.phone_confirmed_at, }, // Include the authentication data session: sessionData, access_token: sessionData?.access_token, refresh_token: sessionData?.refresh_token, auth_url: sessionData?.properties?.action_link || null, redirect_url: finalRedirectUrl, }), { headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } catch (error) { console.error("=== VERIFY-OTP FUNCTION ERROR ==="); console.error("Error details:", error); console.error("Error stack:", error.stack); return new Response( JSON.stringify({ error: "Internal server error", details: error.message, }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json", }, } ); } }); ``` ## Step 5: Set Up Webhook 1. Get your edge function URL from Supabase Dashboard 2. Set your bot webhook by calling: ```bash curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" \ -H "Content-Type: application/json" \ -d '{"url": "https://<YOUR_PROJECT_ID>.supabase.co/functions/v1/telegram-webhook"}' ``` ## Step 6: Copy Components 1. Copy the `telegram-verification-components` folder to your `src` directory 2. Install required dependencies: ```bash npm install @supabase/supabase-js lucide-react input-otp sonner ``` ## Step 7: Integration ### Basic Setup ```tsx // In your main App.tsx or layout file import { TelegramVerificationProvider } from "gebeya-telegram-otp"; import { supabase } from "./integrations/supabase/client"; function App() { return ( <TelegramVerificationProvider supabaseClient={supabase} telegramConfig={{ botName: "@your_bot_name" }} onSuccess={(userData) => { console.log("Verification successful:", userData); }} > <YourAppContent /> </TelegramVerificationProvider> ); } ``` ### Using the Button ```tsx import { TelegramVerifyButton } from "gebeya-telegram-otp"; function LoginPage() { return ( <TelegramVerifyButton buttonText="Verify with Telegram" onVerificationComplete={(userData) => { // Handle successful verification console.log("User verified:", userData); }} /> ); } ``` ## Testing 1. Deploy your edge functions 2. Set up the webhook 3. Test the verification flow: - Enter a phone number - Scan QR code or click Telegram link - Share contact in Telegram - Enter OTP code - Verify successful authentication ## Troubleshooting ### Common Issues 1. **Webhook not working**: Check edge function logs in Supabase 2. **Bot not responding**: Verify bot token and webhook URL 3. **Database errors**: Check RLS policies and table structure 4. **OTP not received**: Check Telegram bot permissions ### Debug Steps 1. Check Supabase edge function logs 2. Test webhook manually with curl 3. Verify database entries in verification_sessions table 4. Check Telegram bot settings with [@BotFather](https://t.me/botfather) ## Security Considerations 1. Never expose bot token in client-side code 2. Implement rate limiting for verification attempts 3. Set appropriate session expiry times 4. Use HTTPS for all webhook URLs 5. Validate phone numbers on both client and server ## Next Steps - Customize styling to match your app - Add additional security measures - Implement user profiles - Add multi-language support - Set up monitoring and analytics