Files
finance_bot/docs/API_INTEGRATION_GUIDE.md
Andrew K. Choi 23a9d975a9 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
2025-12-11 21:00:34 +09:00

7.3 KiB

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

POST /api/v1/auth/register

{
    "email": "user@example.com",
    "password": "securepass123",
    "first_name": "John",
    "last_name": "Doe"
}

Response:

{
    "success": true,
    "user_id": 123,
    "message": "User registered successfully",
    "access_token": "eyJ...",
    "refresh_token": "eyJ...",
    "expires_in": 900
}

Step 2: Bot Uses Token

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

POST /api/v1/auth/telegram/start

{
    "chat_id": 556399210
}

Response:

{
    "code": "PgmL5ZD8vK...",
    "expires_in": 600
}

Step 2: User Confirms Binding (Frontend)

User clicks link and confirms in web/app:

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

POST /api/v1/auth/telegram/register

{
    "chat_id": 556399210,
    "username": "john_doe",
    "first_name": "John"
}

Or get token for existing user:

POST /api/v1/auth/token/get

{
    "chat_id": 556399210
}

Response:

{
    "success": true,
    "access_token": "eyJ...",
    "expires_in": 900,
    "user_id": 123
}

3. Bot Implementation Example

In Python Bot Code

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

# 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

# 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

# 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

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