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

490
docs/API_ENDPOINTS.md Normal file
View File

@@ -0,0 +1,490 @@
# 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)"
}
```
### 4. Start Telegram Binding (Link Account)
```
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)
```python
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)
```javascript
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
```json
{
"detail": "Email already registered"
}
```
### 401 Unauthorized
```json
{
"detail": "Invalid credentials"
}
```
### 404 Not Found
```json
{
"detail": "User not found for this Telegram ID"
}
```
### 500 Internal Server Error
```json
{
"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
```bash
# 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:
```bash
# Inside container or with Python environment
alembic upgrade head
# Check migration status
alembic current
```