guidly/api/_lib/auth.ts
laruevin d5ed7fdcf9 Initial commit: Guidly project with CI/CD pipeline
Telegram Bot + Mini App for city walking quests.
- React 19 + TypeScript + Vite 6 frontend
- Express 5 + PostgreSQL backend
- grammY Telegram bot with DeepSeek AI
- GitLab CI/CD: lint, build, deploy to production

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:42:42 +07:00

117 lines
3.7 KiB
TypeScript

import crypto from 'crypto'
import type { ApiRequest, ApiResponse } from './types.js'
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || ''
const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY || 'dev-internal-key'
const ALLOWED_ORIGINS = [
'https://web.telegram.org',
process.env.APP_URL || null,
process.env.NODE_ENV === 'development' ? 'http://localhost:5173' : null,
].filter(Boolean) as string[]
// Validate Telegram Mini App initData
// https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
export function validateTelegramInitData(initData: string): { id: number; username?: string; first_name?: string; last_name?: string; language_code?: string } | null {
if (!initData || !TELEGRAM_BOT_TOKEN) return null
try {
const params = new URLSearchParams(initData)
const hash = params.get('hash')
if (!hash) return null
params.delete('hash')
// Sort and create data-check-string
const dataCheckString = Array.from(params.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('\n')
// Create secret key
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(TELEGRAM_BOT_TOKEN)
.digest()
// Compute HMAC
const computedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex')
if (computedHash !== hash) return null
// Check auth_date is not too old (24 hours)
const authDate = params.get('auth_date')
if (authDate) {
const authTimestamp = parseInt(authDate) * 1000
const now = Date.now()
if (now - authTimestamp > 24 * 60 * 60 * 1000) return null
}
// Parse user data
const userStr = params.get('user')
if (!userStr) return null
return JSON.parse(userStr)
} catch {
return null
}
}
// Middleware: authenticate via Telegram initData
export function withTelegramAuth(handler: Function) {
return async (req: ApiRequest, res: ApiResponse) => {
const initData = req.headers['x-telegram-init-data'] as string
const user = validateTelegramInitData(initData)
if (!user) {
// In development, allow bypass with query param
if (process.env.NODE_ENV === 'development' && req.query.telegram_id) {
req.telegramUser = {
id: parseInt(req.query.telegram_id as string),
first_name: 'Dev',
language_code: 'ru',
}
return handler(req, res)
}
return res.status(401).json({ error: 'Invalid Telegram authentication' })
}
req.telegramUser = user
return handler(req, res)
}
}
// Middleware: authenticate internal bot-to-API calls
export function withInternalAuth(handler: Function) {
return async (req: ApiRequest, res: ApiResponse) => {
const apiKey = req.headers['x-internal-api-key'] as string
if (apiKey !== INTERNAL_API_KEY) {
return res.status(401).json({ error: 'Invalid internal API key' })
}
return handler(req, res)
}
}
// CORS handler
export function cors(req: ApiRequest, res: ApiResponse) {
const origin = req.headers.origin
if (origin && ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin)
} else if (process.env.NODE_ENV === 'development' && origin) {
if (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) {
res.setHeader('Access-Control-Allow-Origin', origin)
}
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Telegram-Init-Data, X-Internal-Api-Key')
res.setHeader('Access-Control-Allow-Credentials', 'true')
}