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

219 lines
6.5 KiB
Python

"""
Authentication Service - User login, Telegram binding, Token management
"""
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, Tuple
import secrets
from sqlalchemy.orm import Session
from app.db.models import User, Session as DBSession, TelegramIdentity
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()