Files
finance_bot/docs/API_ENDPOINTS.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

9.9 KiB

API Endpoints Reference

Authentication Endpoints

1. User Registration (Email)

POST /api/v1/auth/register
Content-Type: application/json

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

Response (200):
{
    "success": true,
    "user_id": 123,
    "message": "User registered successfully",
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_in": 900
}

2. Get Token for Telegram User

POST /api/v1/auth/token/get
Content-Type: application/json

{
    "chat_id": 556399210
}

Response (200):
{
    "success": true,
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_in": 900,
    "user_id": 123
}

3. Quick Telegram Registration

POST /api/v1/auth/telegram/register
Query Parameters:
- chat_id: 556399210
- username: john_doe
- first_name: John
- last_name: (optional)

Response (200):
{
    "success": true,
    "created": true,
    "user_id": 123,
    "jwt_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "message": "User created successfully (user_id=123)"
}
POST /api/v1/auth/telegram/start
Content-Type: application/json

{
    "chat_id": 556399210
}

Response (200):
{
    "code": "PgmL5ZD8vK2mN3oP4qR5sT6uV7wX8yZ9",
    "expires_in": 600
}

5. Confirm Telegram Binding

POST /api/v1/auth/telegram/confirm
Content-Type: application/json
Authorization: Bearer <user_jwt_token>

{
    "code": "PgmL5ZD8vK2mN3oP4qR5sT6uV7wX8yZ9",
    "chat_id": 556399210,
    "username": "john_doe",
    "first_name": "John",
    "last_name": "Doe"
}

Response (200):
{
    "success": true,
    "user_id": 123,
    "jwt_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_at": "2025-12-12T12:00:00"
}

6. User Login (Email)

POST /api/v1/auth/login
Content-Type: application/json

{
    "email": "user@example.com",
    "password": "securepass123"
}

Response (200):
{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user_id": 123,
    "expires_in": 900
}

7. Refresh Token

POST /api/v1/auth/refresh
Content-Type: application/json

{
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Response (200):
{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_in": 900
}

8. Refresh Token (Telegram)

POST /api/v1/auth/token/refresh-telegram
Query Parameters:
- chat_id: 556399210

Response (200):
{
    "success": true,
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_in": 900,
    "user_id": 123
}

9. Logout

POST /api/v1/auth/logout
Authorization: Bearer <access_token>

Response (200):
{
    "message": "Logged out successfully"
}

Bot Usage Examples

Python (aiohttp)

import aiohttp
import asyncio

class FinanceBotAPI:
    def __init__(self, base_url="http://web:8000"):
        self.base_url = base_url
        self.session = None
    
    async def start(self):
        self.session = aiohttp.ClientSession()
    
    async def register_telegram_user(self, chat_id, username, first_name):
        """Quick register Telegram user"""
        url = f"{self.base_url}/api/v1/auth/telegram/register"
        
        async with self.session.post(
            url,
            params={
                "chat_id": chat_id,
                "username": username,
                "first_name": first_name,
            }
        ) as resp:
            return await resp.json()
    
    async def get_token(self, chat_id):
        """Get fresh token for chat_id"""
        url = f"{self.base_url}/api/v1/auth/token/get"
        
        async with self.session.post(
            url,
            json={"chat_id": chat_id}
        ) as resp:
            return await resp.json()
    
    async def make_request(self, method, endpoint, chat_id, **kwargs):
        """Make authenticated request"""
        # Get token
        token_resp = await self.get_token(chat_id)
        token = token_resp["access_token"]
        
        # Make request
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        }
        
        url = f"{self.base_url}{endpoint}"
        
        async with self.session.request(
            method,
            url,
            headers=headers,
            **kwargs
        ) as resp:
            return await resp.json()
    
    async def close(self):
        await self.session.close()


# Usage
async def main():
    api = FinanceBotAPI()
    await api.start()
    
    # Register new user
    result = await api.register_telegram_user(
        chat_id=556399210,
        username="john_doe",
        first_name="John"
    )
    print(result)
    
    # Get token
    token_resp = await api.get_token(556399210)
    print(token_resp)
    
    # Make authenticated request
    accounts = await api.make_request(
        "GET",
        "/api/v1/accounts",
        chat_id=556399210
    )
    print(accounts)
    
    await api.close()

asyncio.run(main())

Node.js (axios)

const axios = require('axios');

class FinanceBotAPI {
    constructor(baseUrl = 'http://web:8000') {
        this.baseUrl = baseUrl;
        this.client = axios.create({
            baseURL: baseUrl
        });
    }
    
    async registerTelegramUser(chatId, username, firstName) {
        const response = await this.client.post(
            '/api/v1/auth/telegram/register',
            {},
            {
                params: {
                    chat_id: chatId,
                    username: username,
                    first_name: firstName
                }
            }
        );
        return response.data;
    }
    
    async getToken(chatId) {
        const response = await this.client.post(
            '/api/v1/auth/token/get',
            { chat_id: chatId }
        );
        return response.data;
    }
    
    async makeRequest(method, endpoint, chatId, data = null) {
        // Get token
        const tokenResp = await this.getToken(chatId);
        const token = tokenResp.access_token;
        
        // Make request
        const headers = {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json'
        };
        
        const config = {
            method: method.toUpperCase(),
            url: endpoint,
            headers: headers
        };
        
        if (data) {
            config.data = data;
        }
        
        const response = await this.client.request(config);
        return response.data;
    }
}

// Usage
const api = new FinanceBotAPI();

(async () => {
    // Register
    const result = await api.registerTelegramUser(
        556399210,
        'john_doe',
        'John'
    );
    console.log(result);
    
    // Get token
    const tokenResp = await api.getToken(556399210);
    console.log(tokenResp);
    
    // Make request
    const accounts = await api.makeRequest(
        'GET',
        '/api/v1/accounts',
        556399210
    );
    console.log(accounts);
})();

Error Responses

400 Bad Request

{
    "detail": "Email already registered"
}

401 Unauthorized

{
    "detail": "Invalid credentials"
}

404 Not Found

{
    "detail": "User not found for this Telegram ID"
}

500 Internal Server Error

{
    "detail": "Internal server error"
}

Token Management

Access Token

  • TTL: 15 minutes (900 seconds)
  • Usage: Use in Authorization: Bearer <token> header
  • Refresh: Use refresh_token to get new access_token

Refresh Token

  • TTL: 30 days (2,592,000 seconds)
  • Storage: Store securely (Redis for bot, localStorage for web)
  • Usage: Call /api/v1/auth/refresh to get new access_token

Telegram Token

  • TTL: 30 days
  • Storage: Redis cache with key chat_id:{id}:jwt
  • Auto-refresh: Call /api/v1/auth/token/refresh-telegram when expired

Security Headers

All authenticated requests should include:

Authorization: Bearer <access_token>
X-Client-Id: telegram_bot
X-Timestamp: <unix_timestamp>
X-Signature: <hmac_sha256_signature>
Content-Type: application/json

Rate Limiting

  • Auth endpoints: 5 requests/minute per IP
  • API endpoints: 100 requests/minute per user
  • Response headers:
    • X-RateLimit-Limit: Total limit
    • X-RateLimit-Remaining: Remaining requests
    • X-RateLimit-Reset: Reset timestamp (Unix)

Testing

cURL Examples

# Register
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": 556399210}'

# Make authenticated request
TOKEN="eyJ..."
curl -X GET http://localhost:8000/api/v1/accounts \
  -H "Authorization: Bearer $TOKEN"

# Quick Telegram register
curl -X POST "http://localhost:8000/api/v1/auth/telegram/register?chat_id=556399210&username=john_doe&first_name=John"

Database Schema

users table

Column Type Notes
id Integer Primary key
email String(255) Unique, nullable
password_hash String(255) SHA256 hash, nullable
telegram_id Integer Unique, nullable
username String(255) Nullable
first_name String(255) Nullable
last_name String(255) Nullable
is_active Boolean Default: true
email_verified Boolean Default: false
created_at DateTime Auto
updated_at DateTime Auto

Migration

To apply database changes:

# Inside container or with Python environment
alembic upgrade head

# Check migration status
alembic current