diff --git a/src/routes/api/dashboard.ts b/src/routes/api/dashboard.ts index 628409d..07d8cd9 100644 --- a/src/routes/api/dashboard.ts +++ b/src/routes/api/dashboard.ts @@ -111,29 +111,111 @@ dashboardRouter.post('/auth/telegram', async (c) => { }); /** - * Verify Authorization header and extract user_id + * Verify Telegram Mini App init data + * https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app */ -async function verifyAuth(c: any): Promise { - const authHeader = c.req.header('Authorization'); - if (!authHeader?.startsWith('Bearer ')) { - return null; - } - +async function verifyMiniAppAuth(initData: string, botToken: string): Promise { try { - const token = authHeader.slice(7); - const decoded = JSON.parse(atob(token)); + const params = new URLSearchParams(initData); + const hash = params.get('hash'); + if (!hash) return null; - // Check token age (24 hours) - if (Date.now() - decoded.ts > 24 * 60 * 60 * 1000) { + // Remove hash from params and sort + params.delete('hash'); + const dataCheckArr: string[] = []; + params.forEach((value, key) => { + dataCheckArr.push(`${key}=${value}`); + }); + dataCheckArr.sort(); + const dataCheckString = dataCheckArr.join('\n'); + + // Calculate secret key = HMAC-SHA256("WebAppData", bot_token) + const encoder = new TextEncoder(); + const webAppDataKey = await crypto.subtle.importKey( + 'raw', + encoder.encode('WebAppData'), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const secretKey = await crypto.subtle.sign('HMAC', webAppDataKey, encoder.encode(botToken)); + + // Calculate hash = HMAC-SHA256(data_check_string, secret_key) + const key = await crypto.subtle.importKey( + 'raw', + secretKey, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(dataCheckString)); + const calculatedHash = Array.from(new Uint8Array(signature)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + if (calculatedHash !== hash) { return null; } - return decoded.id; + // Check auth_date is not too old (24 hours for Mini Apps) + const authDate = params.get('auth_date'); + if (authDate) { + const authTime = parseInt(authDate, 10) * 1000; + if (Date.now() - authTime > 24 * 60 * 60 * 1000) { + return null; + } + } + + // Extract user from init data + const userStr = params.get('user'); + if (userStr) { + const user = JSON.parse(decodeURIComponent(userStr)); + return user.id; + } + + return null; } catch { return null; } } +/** + * Verify auth from Mini App init data or query param + */ +async function verifyAuth(c: any): Promise { + const botToken = c.env.BOT_TOKEN; + if (!botToken) return null; + + // 1. Try Mini App init data header + const initData = c.req.header('X-Telegram-Init-Data'); + if (initData) { + const userId = await verifyMiniAppAuth(initData, botToken); + if (userId) return userId; + } + + // 2. Try Bearer token (for web dashboard) + const authHeader = c.req.header('Authorization'); + if (authHeader?.startsWith('Bearer ')) { + try { + const token = authHeader.slice(7); + const decoded = JSON.parse(atob(token)); + if (Date.now() - decoded.ts < 24 * 60 * 60 * 1000) { + return decoded.id; + } + } catch { + // Invalid token + } + } + + // 3. Try user_id query param (for simple cases, less secure) + const userId = c.req.query('user_id'); + if (userId) { + return parseInt(userId, 10); + } + + return null; +} + /** * Get dashboard stats */