gebeya-telegram-otp
Version:
Reusable Telegram phone verification components for React applications
1,411 lines (1,359 loc) • 49.3 kB
Markdown
# 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 [](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., ``)
## 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}-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-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 [](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