aut flow
This commit is contained in:
409
.history/app/api/auth_20251210221448.py
Normal file
409
.history/app/api/auth_20251210221448.py
Normal file
@@ -0,0 +1,409 @@
|
||||
"""
|
||||
Authentication API Endpoints - Login, Token Management, Telegram Binding
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.security.jwt_manager import jwt_manager
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/v1/auth", tags=["authentication"])
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class LoginRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
user_id: int
|
||||
expires_in: int # seconds
|
||||
|
||||
|
||||
class TelegramBindingStartRequest(BaseModel):
|
||||
chat_id: int
|
||||
|
||||
|
||||
class TelegramBindingStartResponse(BaseModel):
|
||||
code: str
|
||||
expires_in: int # seconds
|
||||
|
||||
|
||||
class TelegramBindingConfirmRequest(BaseModel):
|
||||
code: str
|
||||
chat_id: int
|
||||
username: Optional[str] = None
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
|
||||
|
||||
class TelegramBindingConfirmResponse(BaseModel):
|
||||
success: bool
|
||||
user_id: int
|
||||
jwt_token: str
|
||||
expires_at: str
|
||||
|
||||
|
||||
class TokenRefreshRequest(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class TokenRefreshResponse(BaseModel):
|
||||
access_token: str
|
||||
expires_in: int
|
||||
|
||||
|
||||
@router.post(
|
||||
"/login",
|
||||
response_model=LoginResponse,
|
||||
summary="User login with email & password",
|
||||
)
|
||||
async def login(
|
||||
request: LoginRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> LoginResponse:
|
||||
"""
|
||||
Authenticate user and create session.
|
||||
|
||||
**Returns:**
|
||||
- access_token: Short-lived JWT (15 min)
|
||||
- refresh_token: Long-lived refresh token (30 days)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
X-Device-Id: device_uuid # For tracking
|
||||
```
|
||||
"""
|
||||
|
||||
# TODO: Verify email + password
|
||||
# For MVP: Assume credentials are valid
|
||||
|
||||
from app.db.models import User
|
||||
|
||||
user = db.query(User).filter(User.email == request.email).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
service = AuthService(db)
|
||||
access_token, refresh_token = await service.create_session(
|
||||
user_id=user.id,
|
||||
device_id=request.__dict__.get("device_id"),
|
||||
)
|
||||
|
||||
return LoginResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
user_id=user.id,
|
||||
expires_in=15 * 60, # 15 minutes
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/refresh",
|
||||
response_model=TokenRefreshResponse,
|
||||
summary="Refresh access token",
|
||||
)
|
||||
async def refresh_token(
|
||||
request: TokenRefreshRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> TokenRefreshResponse:
|
||||
"""
|
||||
Issue new access token using refresh token.
|
||||
|
||||
**Flow:**
|
||||
1. Access token expires
|
||||
2. Send refresh_token to this endpoint
|
||||
3. Receive new access_token (without creating new session)
|
||||
"""
|
||||
|
||||
try:
|
||||
token_payload = jwt_manager.verify_token(request.refresh_token)
|
||||
if token_payload.type != "refresh":
|
||||
raise ValueError("Not a refresh token")
|
||||
|
||||
service = AuthService(db)
|
||||
new_access_token = await service.refresh_access_token(
|
||||
refresh_token=request.refresh_token,
|
||||
user_id=token_payload.sub,
|
||||
)
|
||||
|
||||
return TokenRefreshResponse(
|
||||
access_token=new_access_token,
|
||||
expires_in=15 * 60,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/telegram/start",
|
||||
response_model=TelegramBindingStartResponse,
|
||||
summary="Start Telegram binding flow",
|
||||
)
|
||||
async def telegram_binding_start(
|
||||
request: TelegramBindingStartRequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Generate binding code for Telegram user.
|
||||
|
||||
**Bot Flow:**
|
||||
1. User sends /start
|
||||
2. Bot calls this endpoint: POST /auth/telegram/start
|
||||
3. Bot receives code and generates link
|
||||
4. Bot sends message with link to user
|
||||
5. User clicks link (goes to confirm endpoint)
|
||||
"""
|
||||
|
||||
service = AuthService(db)
|
||||
code = await service.create_telegram_binding_code(chat_id=request.chat_id)
|
||||
|
||||
return TelegramBindingStartResponse(
|
||||
code=code,
|
||||
expires_in=600, # 10 minutes
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/telegram/confirm",
|
||||
response_model=TelegramBindingConfirmResponse,
|
||||
summary="Confirm Telegram binding",
|
||||
)
|
||||
async def telegram_binding_confirm(
|
||||
request: TelegramBindingConfirmRequest,
|
||||
current_request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Confirm Telegram binding and issue JWT.
|
||||
|
||||
**Flow:**
|
||||
1. User logs in or creates account
|
||||
2. User clicks binding link with code
|
||||
3. Frontend calls this endpoint with code + user context
|
||||
4. Backend creates TelegramIdentity record
|
||||
5. Backend returns JWT for bot to use
|
||||
|
||||
**Bot Usage:**
|
||||
```python
|
||||
# Bot stores JWT for user
|
||||
redis.setex(f"chat_id:{chat_id}:jwt", 86400*30, jwt_token)
|
||||
|
||||
# Bot makes API calls
|
||||
api_request.headers['Authorization'] = f'Bearer {jwt_token}'
|
||||
```
|
||||
"""
|
||||
|
||||
# Get authenticated user from JWT
|
||||
user_id = getattr(current_request.state, "user_id", None)
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User must be authenticated")
|
||||
|
||||
service = AuthService(db)
|
||||
result = await service.confirm_telegram_binding(
|
||||
user_id=user_id,
|
||||
chat_id=request.chat_id,
|
||||
code=request.code,
|
||||
username=request.username,
|
||||
first_name=request.first_name,
|
||||
last_name=request.last_name,
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=400, detail="Binding failed")
|
||||
|
||||
return TelegramBindingConfirmResponse(**result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/telegram/store-token",
|
||||
response_model=dict,
|
||||
summary="Store JWT token for Telegram user (called after binding confirmation)",
|
||||
)
|
||||
async def telegram_store_token(
|
||||
chat_id: int,
|
||||
jwt_token: str,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Store JWT token in Redis after successful binding confirmation.
|
||||
|
||||
**Flow:**
|
||||
1. User confirms binding via /telegram/confirm
|
||||
2. Frontend receives jwt_token
|
||||
3. Frontend calls this endpoint to cache token in bot's Redis
|
||||
4. Bot can now use token for API calls
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
POST /auth/telegram/store-token?chat_id=12345&jwt_token=eyJ...
|
||||
```
|
||||
"""
|
||||
|
||||
import redis
|
||||
|
||||
# Get Redis client from settings
|
||||
from app.core.config import settings
|
||||
redis_client = redis.from_url(settings.redis_url)
|
||||
|
||||
# Validate JWT token structure
|
||||
try:
|
||||
jwt_manager.verify_token(jwt_token)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid token: {e}")
|
||||
|
||||
# Store JWT in Redis with 30-day TTL
|
||||
cache_key = f"chat_id:{chat_id}:jwt"
|
||||
redis_client.setex(cache_key, 86400 * 30, jwt_token)
|
||||
|
||||
logger.info(f"JWT token stored for chat_id={chat_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Token stored successfully",
|
||||
"chat_id": chat_id,
|
||||
}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/telegram/authenticate",
|
||||
response_model=dict,
|
||||
summary="Authenticate by Telegram chat_id",
|
||||
)
|
||||
async def telegram_authenticate(
|
||||
chat_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get JWT token for Telegram user (bot authentication).
|
||||
|
||||
**Usage in Bot:**
|
||||
```python
|
||||
# Get token for authenticated user
|
||||
response = api.post("/auth/telegram/authenticate?chat_id=12345")
|
||||
jwt_token = response["jwt_token"]
|
||||
```
|
||||
"""
|
||||
|
||||
service = AuthService(db)
|
||||
result = await service.authenticate_telegram_user(chat_id=chat_id)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Telegram identity not found")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post(
|
||||
"/telegram/register",
|
||||
response_model=dict,
|
||||
summary="Create new user with Telegram binding",
|
||||
)
|
||||
async def telegram_register(
|
||||
chat_id: int,
|
||||
username: Optional[str] = None,
|
||||
first_name: Optional[str] = None,
|
||||
last_name: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Quick registration for new Telegram user.
|
||||
|
||||
**Flow:**
|
||||
1. Bot calls this endpoint on /start
|
||||
2. Creates new User with telegram_id
|
||||
3. Returns JWT for immediate API access
|
||||
4. User can update email/password later
|
||||
|
||||
**Usage in Bot:**
|
||||
```python
|
||||
result = api.post(
|
||||
"/auth/telegram/register",
|
||||
params={
|
||||
"chat_id": 12345,
|
||||
"username": "john_doe",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
}
|
||||
)
|
||||
jwt_token = result["jwt_token"]
|
||||
```
|
||||
"""
|
||||
|
||||
from app.db.models.user import User
|
||||
|
||||
# Check if user already exists
|
||||
existing = db.query(User).filter_by(telegram_id=chat_id).first()
|
||||
if existing:
|
||||
service = AuthService(db)
|
||||
result = await service.authenticate_telegram_user(chat_id=chat_id)
|
||||
return {
|
||||
**result,
|
||||
"created": False,
|
||||
"message": "User already exists",
|
||||
}
|
||||
|
||||
# Create new user
|
||||
new_user = User(
|
||||
telegram_id=chat_id,
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
try:
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to create user: {e}")
|
||||
raise HTTPException(status_code=400, detail="Failed to create user")
|
||||
|
||||
# Create JWT
|
||||
service = AuthService(db)
|
||||
result = await service.authenticate_telegram_user(chat_id=chat_id)
|
||||
|
||||
if result:
|
||||
result["created"] = True
|
||||
result["message"] = f"User created successfully (user_id={new_user.id})"
|
||||
|
||||
logger.info(f"New Telegram user registered: chat_id={chat_id}, user_id={new_user.id}")
|
||||
|
||||
return result or {"success": False, "error": "Failed to create user"}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/logout",
|
||||
summary="Logout user",
|
||||
)
|
||||
async def logout(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Revoke session and blacklist tokens.
|
||||
|
||||
**TODO:** Implement token blacklisting in Redis
|
||||
"""
|
||||
|
||||
user_id = getattr(request.state, "user_id", None)
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
# TODO: Add token to Redis blacklist
|
||||
# redis.setex(f"blacklist:{token}", token_expiry_time, "1")
|
||||
|
||||
return {"message": "Logged out successfully"}
|
||||
Reference in New Issue
Block a user