Files
finance_bot/.history/app/services/auth_service_20251210212101.py
2025-12-10 22:09:31 +09:00

219 lines
6.4 KiB
Python

"""
Authentication Service - User login, token management
"""
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
import secrets
from sqlalchemy.orm import Session
from app.db.models import User
from app.security.jwt_manager import jwt_manager
import logging
logger = logging.getLogger(__name__)
class AuthService:
"""
Handles user authentication, token management, and Telegram binding.
"""
# Configuration
TELEGRAM_BINDING_CODE_TTL = 600 # 10 minutes
BINDING_CODE_LENGTH = 24
def __init__(self, db: Session):
self.db = db
async def create_telegram_binding_code(self, chat_id: int) -> str:
"""
Generate temporary code for Telegram user binding.
Flow:
1. User sends /start to bot
2. Bot generates binding code
3. Bot sends user a link: https://app.com/auth/telegram?code=ABC123
4. User clicks link (authenticated or creates account)
5. Code is confirmed, JWT issued
Returns:
Binding code (24-char random)
"""
# Generate code
code = secrets.token_urlsafe(TELEGRAM_BINDING_CODE_LENGTH)
# Store in Redis (would be: redis.setex(f"telegram:code:{code}", TTL, chat_id))
# For MVP: Store in memory or DB with expiry
logger.info(f"Generated Telegram binding code for chat_id={chat_id}")
return code
async def confirm_telegram_binding(
self,
user_id: int,
chat_id: int,
code: str,
username: Optional[str] = None,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
) -> Dict[str, Any]:
"""
Confirm Telegram binding and create identity.
Returns:
{
"success": true,
"user_id": 123,
"chat_id": 12345,
"jwt_token": "eyJhbGc...",
"expires_at": "2024-01-09T12:30:00Z"
}
"""
# Verify code (would check Redis)
# For MVP: Assume code is valid
# Create or update Telegram identity
identity = self.db.query(TelegramIdentity).filter(
TelegramIdentity.user_id == user_id
).first()
if identity:
# Update existing
identity.chat_id = chat_id
identity.username = username
identity.first_name = first_name
identity.last_name = last_name
identity.verified_at = datetime.utcnow()
identity.updated_at = datetime.utcnow()
else:
# Create new
identity = TelegramIdentity(
user_id=user_id,
chat_id=chat_id,
username=username,
first_name=first_name,
last_name=last_name,
verified_at=datetime.utcnow(),
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
self.db.add(identity)
# Generate JWT token for bot
jwt_token = jwt_manager.create_access_token(
user_id=user_id,
family_ids=[], # Will be loaded from user's families
)
self.db.commit()
logger.info(f"Telegram binding confirmed: user_id={user_id}, chat_id={chat_id}")
return {
"success": True,
"user_id": user_id,
"chat_id": chat_id,
"jwt_token": jwt_token,
"expires_at": (datetime.utcnow() + timedelta(minutes=15)).isoformat() + "Z",
}
async def authenticate_telegram_user(
self,
chat_id: int,
) -> Optional[Dict[str, Any]]:
"""
Authenticate user by Telegram chat_id.
Returns:
{
"user_id": 123,
"chat_id": 12345,
"jwt_token": "...",
}
Or None if not found
"""
# Find Telegram identity
identity = self.db.query(TelegramIdentity).filter(
TelegramIdentity.chat_id == chat_id,
TelegramIdentity.verified_at.isnot(None),
).first()
if not identity:
return None
# Generate JWT
jwt_token = jwt_manager.create_access_token(
user_id=identity.user_id,
)
return {
"user_id": identity.user_id,
"chat_id": chat_id,
"jwt_token": jwt_token,
}
async def create_session(
self,
user_id: int,
device_id: Optional[str] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None,
) -> Tuple[str, str]:
"""
Create new session with refresh token.
Returns:
(access_token, refresh_token)
"""
# Create access token
access_token = jwt_manager.create_access_token(user_id=user_id, device_id=device_id)
# Create refresh token
refresh_token = jwt_manager.create_refresh_token(user_id=user_id, device_id=device_id)
# Store session in DB
session = DBSession(
user_id=user_id,
refresh_token_hash=self._hash_token(refresh_token),
device_id=device_id,
ip_address=ip_address,
user_agent=user_agent,
expires_at=datetime.utcnow() + timedelta(days=30),
created_at=datetime.utcnow(),
)
self.db.add(session)
self.db.commit()
return access_token, refresh_token
async def refresh_access_token(
self,
refresh_token: str,
user_id: int,
) -> str:
"""Issue new access token using refresh token"""
# Verify refresh token
try:
token_payload = jwt_manager.verify_token(refresh_token)
if token_payload.type != "refresh":
raise ValueError("Not a refresh token")
except ValueError:
raise ValueError("Invalid refresh token")
# Create new access token
new_access_token = jwt_manager.create_access_token(user_id=user_id)
return new_access_token
@staticmethod
def _hash_token(token: str) -> str:
"""Hash token for storage"""
import hashlib
return hashlib.sha256(token.encode()).hexdigest()