- 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
427 lines
11 KiB
Python
427 lines
11 KiB
Python
"""
|
|
Example: Using Finance Bot API from Python
|
|
|
|
This file shows how to use the new API endpoints
|
|
to authenticate bot users and make API calls.
|
|
"""
|
|
|
|
import aiohttp
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from typing import Optional, Dict, Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FinanceBotAPIClient:
|
|
"""
|
|
Client for Finance Bot API with authentication support.
|
|
|
|
Features:
|
|
- Email/password registration
|
|
- Telegram user quick registration
|
|
- Telegram binding/linking
|
|
- Token management and refresh
|
|
- Authenticated API calls
|
|
"""
|
|
|
|
def __init__(self, api_base_url: str = "http://web:8000"):
|
|
self.api_base_url = api_base_url
|
|
self.session: Optional[aiohttp.ClientSession] = None
|
|
|
|
async def start(self):
|
|
"""Start HTTP session"""
|
|
self.session = aiohttp.ClientSession()
|
|
|
|
async def close(self):
|
|
"""Close HTTP session"""
|
|
if self.session:
|
|
await self.session.close()
|
|
|
|
# ============ EMAIL AUTHENTICATION ============
|
|
|
|
async def register_email_user(
|
|
self,
|
|
email: str,
|
|
password: str,
|
|
first_name: Optional[str] = None,
|
|
last_name: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Register new user with email and password.
|
|
|
|
Returns:
|
|
{
|
|
"success": True,
|
|
"user_id": 123,
|
|
"access_token": "eyJ...",
|
|
"refresh_token": "eyJ...",
|
|
"expires_in": 900
|
|
}
|
|
"""
|
|
url = f"{self.api_base_url}/api/v1/auth/register"
|
|
|
|
payload = {
|
|
"email": email,
|
|
"password": password,
|
|
"first_name": first_name,
|
|
"last_name": last_name,
|
|
}
|
|
|
|
async with self.session.post(url, json=payload) as resp:
|
|
return await resp.json()
|
|
|
|
async def login_email_user(
|
|
self,
|
|
email: str,
|
|
password: str,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Login user with email and password.
|
|
|
|
Returns:
|
|
{
|
|
"access_token": "eyJ...",
|
|
"refresh_token": "eyJ...",
|
|
"user_id": 123,
|
|
"expires_in": 900
|
|
}
|
|
"""
|
|
url = f"{self.api_base_url}/api/v1/auth/login"
|
|
|
|
payload = {
|
|
"email": email,
|
|
"password": password,
|
|
}
|
|
|
|
async with self.session.post(url, json=payload) as resp:
|
|
return await resp.json()
|
|
|
|
# ============ TELEGRAM AUTHENTICATION ============
|
|
|
|
async def quick_register_telegram_user(
|
|
self,
|
|
chat_id: int,
|
|
username: Optional[str] = None,
|
|
first_name: Optional[str] = None,
|
|
last_name: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Quick register Telegram user (one API call).
|
|
|
|
Perfect for bot /register command.
|
|
|
|
Returns:
|
|
{
|
|
"success": True,
|
|
"user_id": 123,
|
|
"jwt_token": "eyJ...",
|
|
"created": True
|
|
}
|
|
"""
|
|
url = f"{self.api_base_url}/api/v1/auth/telegram/register"
|
|
|
|
params = {
|
|
"chat_id": chat_id,
|
|
"username": username or f"user_{chat_id}",
|
|
"first_name": first_name,
|
|
"last_name": last_name,
|
|
}
|
|
|
|
# Remove None values
|
|
params = {k: v for k, v in params.items() if v is not None}
|
|
|
|
async with self.session.post(url, params=params) as resp:
|
|
return await resp.json()
|
|
|
|
async def start_telegram_binding(
|
|
self,
|
|
chat_id: int,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Start Telegram binding process (for linking existing accounts).
|
|
|
|
Returns:
|
|
{
|
|
"code": "PgmL5ZD8vK2mN3oP4qR5sT6uV7wX8yZ9",
|
|
"expires_in": 600
|
|
}
|
|
"""
|
|
url = f"{self.api_base_url}/api/v1/auth/telegram/start"
|
|
|
|
payload = {"chat_id": chat_id}
|
|
|
|
async with self.session.post(url, json=payload) as resp:
|
|
return await resp.json()
|
|
|
|
async def get_token_for_telegram_user(
|
|
self,
|
|
chat_id: int,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Get JWT token for Telegram user.
|
|
|
|
Use after binding confirmation or registration.
|
|
|
|
Returns:
|
|
{
|
|
"success": True,
|
|
"access_token": "eyJ...",
|
|
"expires_in": 900,
|
|
"user_id": 123
|
|
}
|
|
"""
|
|
url = f"{self.api_base_url}/api/v1/auth/token/get"
|
|
|
|
payload = {"chat_id": chat_id}
|
|
|
|
async with self.session.post(url, json=payload) as resp:
|
|
return await resp.json()
|
|
|
|
async def refresh_telegram_token(
|
|
self,
|
|
chat_id: int,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Refresh token for Telegram user.
|
|
|
|
Returns:
|
|
{
|
|
"success": True,
|
|
"access_token": "eyJ...",
|
|
"expires_in": 900,
|
|
"user_id": 123
|
|
}
|
|
"""
|
|
url = f"{self.api_base_url}/api/v1/auth/token/refresh-telegram"
|
|
|
|
params = {"chat_id": chat_id}
|
|
|
|
async with self.session.post(url, params=params) as resp:
|
|
return await resp.json()
|
|
|
|
# ============ API CALLS WITH AUTHENTICATION ============
|
|
|
|
async def make_authenticated_request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
access_token: str,
|
|
data: Optional[Dict] = None,
|
|
params: Optional[Dict] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Make authenticated API request.
|
|
|
|
Args:
|
|
method: GET, POST, PUT, DELETE
|
|
endpoint: /api/v1/accounts, etc.
|
|
access_token: JWT token from login/register
|
|
data: Request body (for POST/PUT)
|
|
params: Query parameters
|
|
|
|
Returns:
|
|
API response JSON
|
|
"""
|
|
url = f"{self.api_base_url}{endpoint}"
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {access_token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
async with self.session.request(
|
|
method,
|
|
url,
|
|
json=data,
|
|
params=params,
|
|
headers=headers,
|
|
) as resp:
|
|
return await resp.json()
|
|
|
|
async def get_accounts(
|
|
self,
|
|
access_token: str,
|
|
family_id: Optional[int] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Get user's accounts"""
|
|
params = {}
|
|
if family_id:
|
|
params["family_id"] = family_id
|
|
|
|
return await self.make_authenticated_request(
|
|
"GET",
|
|
"/api/v1/accounts",
|
|
access_token,
|
|
params=params,
|
|
)
|
|
|
|
async def get_balance(
|
|
self,
|
|
access_token: str,
|
|
account_id: int,
|
|
) -> Dict[str, Any]:
|
|
"""Get account balance"""
|
|
return await self.make_authenticated_request(
|
|
"GET",
|
|
f"/api/v1/accounts/{account_id}",
|
|
access_token,
|
|
)
|
|
|
|
async def create_transaction(
|
|
self,
|
|
access_token: str,
|
|
amount: float,
|
|
category: str,
|
|
description: Optional[str] = None,
|
|
**kwargs
|
|
) -> Dict[str, Any]:
|
|
"""Create new transaction"""
|
|
data = {
|
|
"amount": amount,
|
|
"category": category,
|
|
"description": description,
|
|
**kwargs
|
|
}
|
|
|
|
return await self.make_authenticated_request(
|
|
"POST",
|
|
"/api/v1/transactions",
|
|
access_token,
|
|
data=data,
|
|
)
|
|
|
|
# ============ TOKEN REFRESH ============
|
|
|
|
async def refresh_access_token(
|
|
self,
|
|
refresh_token: str,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Refresh access token using refresh token.
|
|
|
|
Returns:
|
|
{
|
|
"access_token": "eyJ...",
|
|
"expires_in": 900
|
|
}
|
|
"""
|
|
url = f"{self.api_base_url}/api/v1/auth/refresh"
|
|
|
|
payload = {"refresh_token": refresh_token}
|
|
|
|
async with self.session.post(url, json=payload) as resp:
|
|
return await resp.json()
|
|
|
|
|
|
# ============ USAGE EXAMPLES ============
|
|
|
|
async def example_quick_registration():
|
|
"""Example: Register Telegram user with one command"""
|
|
api = FinanceBotAPIClient()
|
|
await api.start()
|
|
|
|
try:
|
|
# User sends /register command
|
|
# Bot registers them
|
|
result = await api.quick_register_telegram_user(
|
|
chat_id=556399210,
|
|
username="john_doe",
|
|
first_name="John",
|
|
last_name="Doe"
|
|
)
|
|
|
|
print("Registration result:", result)
|
|
|
|
if result.get("success"):
|
|
jwt_token = result.get("jwt_token")
|
|
user_id = result.get("user_id")
|
|
|
|
print(f"✅ User {user_id} registered!")
|
|
print(f"Token: {jwt_token[:50]}...")
|
|
|
|
# Now can make API calls with this token
|
|
# Store in Redis for later use
|
|
|
|
finally:
|
|
await api.close()
|
|
|
|
|
|
async def example_get_balance():
|
|
"""Example: Get user's account balance"""
|
|
api = FinanceBotAPIClient()
|
|
await api.start()
|
|
|
|
try:
|
|
# First, get token
|
|
token_result = await api.get_token_for_telegram_user(
|
|
chat_id=556399210
|
|
)
|
|
|
|
if not token_result.get("success"):
|
|
print("❌ Could not get token")
|
|
return
|
|
|
|
access_token = token_result["access_token"]
|
|
|
|
# Make API call
|
|
accounts = await api.get_accounts(access_token)
|
|
|
|
print("Accounts:", accounts)
|
|
|
|
finally:
|
|
await api.close()
|
|
|
|
|
|
async def example_link_account():
|
|
"""Example: Link existing account to Telegram"""
|
|
api = FinanceBotAPIClient()
|
|
await api.start()
|
|
|
|
try:
|
|
# Start binding process
|
|
code_result = await api.start_telegram_binding(
|
|
chat_id=556399210
|
|
)
|
|
|
|
binding_code = code_result.get("code")
|
|
|
|
print(f"Binding code: {binding_code}")
|
|
print(f"Expires in: {code_result.get('expires_in')} seconds")
|
|
|
|
# User confirms binding on web
|
|
# Bot then gets token
|
|
|
|
# After confirmation, get token
|
|
token_result = await api.get_token_for_telegram_user(
|
|
chat_id=556399210
|
|
)
|
|
|
|
if token_result.get("success"):
|
|
print(f"✅ Account linked! User ID: {token_result['user_id']}")
|
|
|
|
finally:
|
|
await api.close()
|
|
|
|
|
|
async def main():
|
|
"""Run examples"""
|
|
print("🤖 Finance Bot API Examples\n")
|
|
|
|
print("1. Quick Registration:")
|
|
print("-" * 50)
|
|
await example_quick_registration()
|
|
|
|
print("\n2. Get Balance:")
|
|
print("-" * 50)
|
|
await example_get_balance()
|
|
|
|
print("\n3. Link Account:")
|
|
print("-" * 50)
|
|
await example_link_account()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|