feat: Complete API authentication system with email & Telegram support

- 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
This commit is contained in:
2025-12-11 21:00:34 +09:00
parent b642d1e9e9
commit 23a9d975a9
21 changed files with 4832 additions and 480 deletions

View File

@@ -0,0 +1,336 @@
# 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