- Add email/password registration endpoint (/api/v1/auth/register) - Add JWT token endpoints for Telegram users (/api/v1/auth/token/get, /api/v1/auth/token/refresh-telegram) - Enhance User model to support both email and Telegram authentication - Fix JWT token handling: convert sub to string (RFC compliance with PyJWT 2.10.1+) - Fix bot API calls: filter None values from query parameters - Fix JWT extraction from Redis: handle both bytes and string returns - Add public endpoints to JWT middleware: /api/v1/auth/register, /api/v1/auth/token/* - Update bot commands: /register (one-tap), /link (account linking), /start (options) - Create complete database schema migration with email auth support - Remove deprecated version attribute from docker-compose.yml - Add service dependency: bot waits for web service startup Features: - Dual authentication: email/password OR Telegram ID - JWT tokens with 15-min access + 30-day refresh lifetime - Redis-based token storage with TTL - Comprehensive API documentation and integration guides - Test scripts and Python examples - Full deployment checklist Database changes: - User model: added email, password_hash, email_verified (nullable fields) - telegram_id now nullable to support email-only users - Complete schema with families, accounts, categories, transactions, budgets, goals Status: Production-ready with all tests passing
337 lines
7.3 KiB
Markdown
337 lines
7.3 KiB
Markdown
# API Integration Guide for Telegram Bot
|
|
|
|
## Overview
|
|
The bot can authenticate users in two ways:
|
|
1. **Email/Password Registration** - Users create account with email
|
|
2. **Telegram Direct Binding** - Direct binding via telegram_id
|
|
|
|
## 1. Email/Password Registration Flow
|
|
|
|
### Step 1: Register User
|
|
```bash
|
|
POST /api/v1/auth/register
|
|
|
|
{
|
|
"email": "user@example.com",
|
|
"password": "securepass123",
|
|
"first_name": "John",
|
|
"last_name": "Doe"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"user_id": 123,
|
|
"message": "User registered successfully",
|
|
"access_token": "eyJ...",
|
|
"refresh_token": "eyJ...",
|
|
"expires_in": 900
|
|
}
|
|
```
|
|
|
|
### Step 2: Bot Uses Token
|
|
```python
|
|
headers = {
|
|
"Authorization": "Bearer eyJ...",
|
|
"Content-Type": "application/json"
|
|
}
|
|
response = requests.get("/api/v1/accounts", headers=headers)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Telegram Direct Binding Flow
|
|
|
|
### Step 1: Generate Binding Code
|
|
```bash
|
|
POST /api/v1/auth/telegram/start
|
|
|
|
{
|
|
"chat_id": 556399210
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"code": "PgmL5ZD8vK...",
|
|
"expires_in": 600
|
|
}
|
|
```
|
|
|
|
### Step 2: User Confirms Binding (Frontend)
|
|
User clicks link and confirms in web/app:
|
|
```bash
|
|
POST /api/v1/auth/telegram/confirm
|
|
|
|
{
|
|
"code": "PgmL5ZD8vK...",
|
|
"chat_id": 556399210,
|
|
"username": "john_doe",
|
|
"first_name": "John",
|
|
"last_name": "Doe"
|
|
}
|
|
```
|
|
|
|
Requires user to be authenticated first (email login).
|
|
|
|
### Step 3: Bot Gets Token
|
|
```bash
|
|
POST /api/v1/auth/telegram/register
|
|
|
|
{
|
|
"chat_id": 556399210,
|
|
"username": "john_doe",
|
|
"first_name": "John"
|
|
}
|
|
```
|
|
|
|
Or get token for existing user:
|
|
```bash
|
|
POST /api/v1/auth/token/get
|
|
|
|
{
|
|
"chat_id": 556399210
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"access_token": "eyJ...",
|
|
"expires_in": 900,
|
|
"user_id": 123
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Bot Implementation Example
|
|
|
|
### In Python Bot Code
|
|
|
|
```python
|
|
import aiohttp
|
|
import asyncio
|
|
from datetime import datetime, timedelta
|
|
|
|
class BotAuthManager:
|
|
def __init__(self, api_base_url: str, redis_client):
|
|
self.api_base_url = api_base_url
|
|
self.redis = redis_client
|
|
self.session = None
|
|
|
|
async def start(self):
|
|
self.session = aiohttp.ClientSession()
|
|
|
|
async def register_user(self, email: str, password: str, name: str) -> dict:
|
|
"""Register new user and return token"""
|
|
response = await self.session.post(
|
|
f"{self.api_base_url}/api/v1/auth/register",
|
|
json={
|
|
"email": email,
|
|
"password": password,
|
|
"first_name": name
|
|
}
|
|
)
|
|
|
|
data = await response.json()
|
|
|
|
if response.status == 200:
|
|
# Store token in Redis
|
|
self.redis.setex(
|
|
f"user:{data['user_id']}:token",
|
|
data['expires_in'],
|
|
data['access_token']
|
|
)
|
|
return data
|
|
else:
|
|
raise Exception(f"Registration failed: {data}")
|
|
|
|
async def bind_telegram(self, chat_id: int, username: str) -> dict:
|
|
"""Quick bind Telegram user"""
|
|
response = await self.session.post(
|
|
f"{self.api_base_url}/api/v1/auth/telegram/register",
|
|
params={
|
|
"chat_id": chat_id,
|
|
"username": username
|
|
}
|
|
)
|
|
|
|
data = await response.json()
|
|
|
|
if response.status == 200 or data.get("success"):
|
|
# Store token for bot
|
|
self.redis.setex(
|
|
f"chat_id:{chat_id}:jwt",
|
|
86400 * 30, # 30 days
|
|
data['jwt_token']
|
|
)
|
|
return data
|
|
else:
|
|
raise Exception(f"Telegram binding failed: {data}")
|
|
|
|
async def get_token(self, chat_id: int) -> str:
|
|
"""Get fresh token for chat_id"""
|
|
response = await self.session.post(
|
|
f"{self.api_base_url}/api/v1/auth/token/get",
|
|
json={"chat_id": chat_id}
|
|
)
|
|
|
|
data = await response.json()
|
|
|
|
if response.status == 200:
|
|
# Store token
|
|
self.redis.setex(
|
|
f"chat_id:{chat_id}:jwt",
|
|
data['expires_in'],
|
|
data['access_token']
|
|
)
|
|
return data['access_token']
|
|
else:
|
|
raise Exception(f"Token fetch failed: {data}")
|
|
|
|
async def make_api_call(self, method: str, endpoint: str, chat_id: int, **kwargs):
|
|
"""Make authenticated API call"""
|
|
token = self.redis.get(f"chat_id:{chat_id}:jwt")
|
|
|
|
if not token:
|
|
# Try to get fresh token
|
|
token = await self.get_token(chat_id)
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {token.decode() if isinstance(token, bytes) else token}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
url = f"{self.api_base_url}{endpoint}"
|
|
|
|
async with self.session.request(method, url, headers=headers, **kwargs) as response:
|
|
return await response.json()
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Error Handling
|
|
|
|
### 400 - Bad Request
|
|
- Invalid email format
|
|
- Missing required fields
|
|
- Email already registered
|
|
|
|
### 401 - Unauthorized
|
|
- Invalid credentials
|
|
- Token expired
|
|
- No authentication provided
|
|
|
|
### 404 - Not Found
|
|
- User not found
|
|
- Chat ID not linked to user
|
|
|
|
### 500 - Server Error
|
|
- Database error
|
|
- Server error
|
|
|
|
---
|
|
|
|
## 5. Token Refresh Strategy
|
|
|
|
### Access Token (15 minutes)
|
|
- Short-lived token for API calls
|
|
- Expires every 15 minutes
|
|
- Automatic refresh with refresh_token
|
|
|
|
### Refresh Token (30 days)
|
|
- Long-lived token to get new access tokens
|
|
- Stored in Redis
|
|
- Use to refresh access_token without re-login
|
|
|
|
### Bot Storage Strategy
|
|
```python
|
|
# On successful binding
|
|
redis.setex(f"chat_id:{chat_id}:jwt", 86400*30, access_token)
|
|
|
|
# Before API call
|
|
token = redis.get(f"chat_id:{chat_id}:jwt")
|
|
if not token:
|
|
token = await get_fresh_token(chat_id)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Environment Variables
|
|
|
|
```bash
|
|
# In docker-compose.yml
|
|
API_BASE_URL=http://web:8000
|
|
BOT_TOKEN=your_telegram_bot_token
|
|
REDIS_URL=redis://redis:6379/0
|
|
DB_PASSWORD=your_db_password
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Testing API Endpoints
|
|
|
|
### Using cURL
|
|
|
|
```bash
|
|
# Register new user
|
|
curl -X POST http://localhost:8000/api/v1/auth/register \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"email": "test@example.com",
|
|
"password": "test123",
|
|
"first_name": "Test"
|
|
}'
|
|
|
|
# Get token for Telegram user
|
|
curl -X POST http://localhost:8000/api/v1/auth/token/get \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"chat_id": 123456}'
|
|
|
|
# Make authenticated request
|
|
curl -X GET http://localhost:8000/api/v1/accounts \
|
|
-H "Authorization: Bearer eyJ..."
|
|
```
|
|
|
|
### Using Python requests
|
|
|
|
```python
|
|
import requests
|
|
|
|
BASE_URL = "http://localhost:8000"
|
|
|
|
# Register
|
|
resp = requests.post(f"{BASE_URL}/api/v1/auth/register", json={
|
|
"email": "test@example.com",
|
|
"password": "test123",
|
|
"first_name": "Test"
|
|
})
|
|
print(resp.json())
|
|
|
|
# Get token
|
|
resp = requests.post(f"{BASE_URL}/api/v1/auth/token/get", json={
|
|
"chat_id": 556399210
|
|
})
|
|
token = resp.json()["access_token"]
|
|
|
|
# Use token
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
resp = requests.get(f"{BASE_URL}/api/v1/accounts", headers=headers)
|
|
print(resp.json())
|
|
```
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. Apply migration: `alembic upgrade head`
|
|
2. Test endpoints with cURL/Postman
|
|
3. Update bot code to use new endpoints
|
|
4. Deploy with docker-compose
|
|
|