diff --git a/package.json b/package.json index 1c3fc8d..7b9fb7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pezkuwi-telegram-miniapp", - "version": "1.0.113", + "version": "1.0.116", "type": "module", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "author": "Pezkuwichain Team", diff --git a/src/version.json b/src/version.json index d3d3940..e36dfcf 100644 --- a/src/version.json +++ b/src/version.json @@ -1,5 +1,5 @@ { - "version": "1.0.113", - "buildTime": "2026-02-05T13:02:30.381Z", - "buildNumber": 1770296550382 + "version": "1.0.116", + "buildTime": "2026-02-06T01:06:49.994Z", + "buildNumber": 1770340009995 } diff --git a/supabase/functions/telegram-auth/index.ts b/supabase/functions/telegram-auth/index.ts index b5209e3..4ee1ed3 100644 --- a/supabase/functions/telegram-auth/index.ts +++ b/supabase/functions/telegram-auth/index.ts @@ -2,11 +2,23 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts'; -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': - 'authorization, x-client-info, apikey, content-type, x-supabase-client-platform', -}; +// CORS - Production domain only +const ALLOWED_ORIGINS = [ + 'https://telegram.pezkuwichain.io', + 'https://t.me', // Telegram WebApp iframe +]; + +function getCorsHeaders(origin: string | null): Record { + const allowedOrigin = + origin && ALLOWED_ORIGINS.some((o) => origin.startsWith(o)) ? origin : ALLOWED_ORIGINS[0]; + + return { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Headers': + 'authorization, x-client-info, apikey, content-type, x-supabase-client-platform', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + }; +} interface TelegramUser { id: number; @@ -17,27 +29,34 @@ interface TelegramUser { language_code?: string; } +// Validate Telegram WebApp initData function validateInitData(initData: string, botToken: string): TelegramUser | null { try { const params = new URLSearchParams(initData); const hash = params.get('hash'); - if (!hash) return null; + if (!hash) { + console.error('[validateInitData] No hash in initData'); + return null; + } params.delete('hash'); - // Sort parameters + // Sort parameters alphabetically const sortedParams = Array.from(params.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}=${value}`) .join('\n'); - // Validate hash + // Calculate secret key: HMAC-SHA256("WebAppData", bot_token) const secretKey = createHmac('sha256', 'WebAppData').update(botToken).digest(); + // Calculate hash: HMAC-SHA256(secret_key, data_check_string) const calculatedHash = createHmac('sha256', secretKey).update(sortedParams).digest('hex'); if (calculatedHash !== hash) { - console.error('Hash mismatch'); + console.error('[validateInitData] Hash mismatch'); + console.error('[validateInitData] Expected:', hash); + console.error('[validateInitData] Calculated:', calculatedHash); return null; } @@ -45,63 +64,122 @@ function validateInitData(initData: string, botToken: string): TelegramUser | nu const authDate = parseInt(params.get('auth_date') || '0'); const now = Math.floor(Date.now() / 1000); if (now - authDate > 86400) { - console.error('Auth data expired'); + console.error('[validateInitData] Auth data expired. Age:', now - authDate, 'seconds'); return null; } // Parse user data const userStr = params.get('user'); - if (!userStr) return null; + if (!userStr) { + console.error('[validateInitData] No user in initData'); + return null; + } - return JSON.parse(userStr) as TelegramUser; + const user = JSON.parse(userStr) as TelegramUser; + console.log('[validateInitData] Success for user:', user.id, user.first_name); + return user; } catch (e) { - console.error('Validation error:', e); + console.error('[validateInitData] Error:', e); return null; } } -// Generate session token -function generateSessionToken(telegramId: number): string { - const payload = `${telegramId}:${Date.now()}:${crypto.randomUUID()}`; - return btoa(payload); +// Session token secret (derived from bot token) +function getSessionSecret(botToken: string): Uint8Array { + return createHmac('sha256', 'SessionTokenSecret').update(botToken).digest(); } -// Verify session token -function verifySessionToken(token: string): number | null { +// Generate HMAC-signed session token +function generateSessionToken(telegramId: number, botToken: string): string { + const payload = { + tgId: telegramId, + iat: Date.now(), + exp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours + jti: crypto.randomUUID(), + }; + + const payloadStr = JSON.stringify(payload); + const payloadB64 = btoa(payloadStr); + + // Sign with HMAC-SHA256 + const secret = getSessionSecret(botToken); + const signature = createHmac('sha256', secret).update(payloadB64).digest('hex'); + + return `${payloadB64}.${signature}`; +} + +// Verify HMAC-signed session token +function verifySessionToken(token: string, botToken: string): number | null { try { - const decoded = atob(token); - const [telegramId, timestamp] = decoded.split(':'); - const ts = parseInt(timestamp); - // Token valid for 7 days - if (Date.now() - ts > 7 * 24 * 60 * 60 * 1000) { + const parts = token.split('.'); + if (parts.length !== 2) { + console.error('[verifySessionToken] Invalid token format'); return null; } - return parseInt(telegramId); - } catch { + + const [payloadB64, signature] = parts; + + // Verify signature + const secret = getSessionSecret(botToken); + const expectedSig = createHmac('sha256', secret).update(payloadB64).digest('hex'); + + if (signature !== expectedSig) { + console.error('[verifySessionToken] Invalid signature'); + return null; + } + + // Parse payload + const payload = JSON.parse(atob(payloadB64)); + + // Check expiration + if (Date.now() > payload.exp) { + console.error('[verifySessionToken] Token expired'); + return null; + } + + return payload.tgId; + } catch (e) { + console.error('[verifySessionToken] Error:', e); return null; } } serve(async (req) => { - // Handle CORS + const origin = req.headers.get('origin'); + const corsHeaders = getCorsHeaders(origin); + + // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } try { const body = await req.json(); - const { initData, telegram_id, from_miniapp, wallet_address, sessionToken } = body; + const { initData, sessionToken } = body; - // Create Supabase admin client + // Get environment variables const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; + const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN'); + + if (!botToken) { + console.error('[telegram-auth] TELEGRAM_BOT_TOKEN not set'); + return new Response(JSON.stringify({ error: 'Server configuration error' }), { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + + // Create Supabase admin client const supabase = createClient(supabaseUrl, supabaseServiceKey); - let telegramUser: TelegramUser | null = null; - + // ======================================== // Method 1: Session token verification + // ======================================== if (sessionToken) { - const tgId = verifySessionToken(sessionToken); + console.log('[telegram-auth] Method 1: Session token verification'); + + const tgId = verifySessionToken(sessionToken, botToken); if (!tgId) { return new Response(JSON.stringify({ error: 'Invalid or expired session' }), { status: 401, @@ -110,81 +188,46 @@ serve(async (req) => { } // Get user by telegram_id - const { data: userData } = await supabase + const { data: userData, error: userError } = await supabase .from('users') .select('*') .eq('telegram_id', tgId) .single(); - if (!userData) { + if (userError || !userData) { + console.error('[telegram-auth] User not found for tgId:', tgId); return new Response(JSON.stringify({ error: 'User not found' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } + // Return user with refreshed token return new Response( JSON.stringify({ user: userData, - session_token: generateSessionToken(tgId), + session_token: generateSessionToken(tgId, botToken), }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } - // Method 2: Mini-app redirect (telegram_id from URL params) - if (from_miniapp && telegram_id) { - // Check if user exists - const { data: existingUser } = await supabase - .from('users') - .select('*') - .eq('telegram_id', telegram_id) - .single(); - - if (!existingUser) { - return new Response( - JSON.stringify({ error: 'User not found. Please use the main app first.' }), - { - status: 404, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - } - ); - } - - // Update wallet if provided - if (wallet_address && wallet_address !== existingUser.wallet_address) { - await supabase.from('users').update({ wallet_address }).eq('telegram_id', telegram_id); - existingUser.wallet_address = wallet_address; - } - - return new Response( - JSON.stringify({ - user: existingUser, - session_token: generateSessionToken(telegram_id), - }), - { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } - ); - } - - // Method 3: Full Telegram WebApp initData verification + // ======================================== + // Method 2: Telegram WebApp initData verification + // ======================================== if (!initData) { + console.error('[telegram-auth] No initData or sessionToken provided'); return new Response(JSON.stringify({ error: 'Missing authentication data' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } - const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN'); - if (!botToken) { - console.error('TELEGRAM_BOT_TOKEN not set'); - return new Response(JSON.stringify({ error: 'Server configuration error' }), { - status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); - } + console.log('[telegram-auth] Method 2: initData verification'); + console.log('[telegram-auth] initData length:', initData.length); // Validate Telegram data - telegramUser = validateInitData(initData, botToken); + const telegramUser = validateInitData(initData, botToken); if (!telegramUser) { return new Response(JSON.stringify({ error: 'Invalid Telegram data' }), { status: 401, @@ -204,6 +247,8 @@ serve(async (req) => { if (existingUser) { // Update existing user userId = existingUser.id; + console.log('[telegram-auth] Updating existing user:', userId); + await supabase .from('users') .update({ @@ -212,10 +257,13 @@ serve(async (req) => { last_name: telegramUser.last_name, photo_url: telegramUser.photo_url, language_code: telegramUser.language_code, + updated_at: new Date().toISOString(), }) .eq('id', userId); } else { // Create new user + console.log('[telegram-auth] Creating new user for telegram_id:', telegramUser.id); + const { data: newUser, error: createError } = await supabase .from('users') .insert({ @@ -230,7 +278,7 @@ serve(async (req) => { .single(); if (createError) { - console.error('Error creating user:', createError); + console.error('[telegram-auth] Error creating user:', createError); return new Response(JSON.stringify({ error: 'Failed to create user' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, @@ -238,21 +286,22 @@ serve(async (req) => { } userId = newUser.id; + console.log('[telegram-auth] New user created:', userId); } - // Get the user data + // Get the full user data const { data: userData } = await supabase.from('users').select('*').eq('id', userId).single(); return new Response( JSON.stringify({ user: userData, telegram_user: telegramUser, - session_token: generateSessionToken(telegramUser.id), + session_token: generateSessionToken(telegramUser.id, botToken), }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { - console.error('Error:', error); + console.error('[telegram-auth] Unexpected error:', error); return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },