aut flow
This commit is contained in:
260
.history/app/services/auth_service_20251210221228.py
Normal file
260
.history/app/services/auth_service_20251210221228.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
Authentication Service - User login, token management, Telegram binding
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
import secrets
|
||||
import json
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.models.user import User
|
||||
from app.security.jwt_manager import jwt_manager
|
||||
from app.core.config import settings
|
||||
import logging
|
||||
import redis
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Redis connection for caching binding codes
|
||||
redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
db=settings.REDIS_DB,
|
||||
decode_responses=True,
|
||||
)
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""Handles user authentication, token management, and Telegram binding"""
|
||||
|
||||
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. Bot calls /auth/telegram/start with chat_id
|
||||
2. Service generates random code and stores in Redis
|
||||
3. Bot builds link: https://bot.example.com/bind?code=XXX
|
||||
4. Bot sends link to user in Telegram
|
||||
5. User clicks link, authenticates, calls /auth/telegram/confirm
|
||||
"""
|
||||
code = secrets.token_urlsafe(self.BINDING_CODE_LENGTH)
|
||||
|
||||
# Store in Redis with TTL (10 minutes)
|
||||
cache_key = f"binding_code:{code}"
|
||||
cache_data = {
|
||||
"chat_id": chat_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
redis_client.setex(
|
||||
cache_key,
|
||||
self.TELEGRAM_BINDING_CODE_TTL,
|
||||
json.dumps(cache_data),
|
||||
)
|
||||
|
||||
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 after user clicks link.
|
||||
|
||||
**Flow:**
|
||||
1. User authenticates with email/password
|
||||
2. User clicks binding link: /bind?code=XXX
|
||||
3. Frontend calls /auth/telegram/confirm with code
|
||||
4. Service validates code from Redis
|
||||
5. Service links user.id with telegram_id
|
||||
6. Service returns JWT for bot to use
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"success": True,
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
}
|
||||
"""
|
||||
|
||||
# Validate code from Redis
|
||||
cache_key = f"binding_code:{code}"
|
||||
cached_data = redis_client.get(cache_key)
|
||||
|
||||
if not cached_data:
|
||||
logger.warning(f"Binding code not found or expired: {code}")
|
||||
return {"success": False, "error": "Code expired"}
|
||||
|
||||
binding_data = json.loads(cached_data)
|
||||
cached_chat_id = binding_data.get("chat_id")
|
||||
|
||||
if cached_chat_id != chat_id:
|
||||
logger.warning(
|
||||
f"Chat ID mismatch: expected {cached_chat_id}, got {chat_id}"
|
||||
)
|
||||
return {"success": False, "error": "Code mismatch"}
|
||||
|
||||
# Get user from database
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
logger.error(f"User not found: {user_id}")
|
||||
return {"success": False, "error": "User not found"}
|
||||
|
||||
# Update user with Telegram info
|
||||
user.telegram_id = chat_id
|
||||
if username:
|
||||
user.username = username
|
||||
if first_name:
|
||||
user.first_name = first_name
|
||||
if last_name:
|
||||
user.last_name = last_name
|
||||
|
||||
user.updated_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
# Create JWT token for bot
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
# Remove code from Redis
|
||||
redis_client.delete(cache_key)
|
||||
|
||||
logger.info(
|
||||
f"Telegram binding confirmed: user_id={user_id}, chat_id={chat_id}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
|
||||
async def create_session(
|
||||
self,
|
||||
user_id: int,
|
||||
device_id: Optional[str] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Create session and issue tokens.
|
||||
|
||||
**Returns:**
|
||||
(access_token, refresh_token)
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# Create tokens
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
refresh_token = jwt_manager.create_refresh_token(user_id=user.id)
|
||||
|
||||
# Store refresh token in Redis for validation
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = {
|
||||
"user_id": user.id,
|
||||
"device_id": device_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Refresh token valid for 30 days
|
||||
redis_client.setex(
|
||||
token_key,
|
||||
86400 * 30,
|
||||
json.dumps(token_data),
|
||||
)
|
||||
|
||||
# Update user activity
|
||||
user.last_activity = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
logger.info(f"Session created for user_id={user_id}")
|
||||
|
||||
return access_token, refresh_token
|
||||
|
||||
async def refresh_access_token(
|
||||
self,
|
||||
refresh_token: str,
|
||||
user_id: int,
|
||||
) -> str:
|
||||
"""
|
||||
Issue new access token using valid refresh token.
|
||||
|
||||
**Flow:**
|
||||
1. Check refresh token in Redis
|
||||
2. Validate it belongs to user_id
|
||||
3. Create new access token
|
||||
4. Return new token (don't invalidate refresh token)
|
||||
"""
|
||||
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = redis_client.get(token_key)
|
||||
|
||||
if not token_data:
|
||||
logger.warning(f"Refresh token not found: {user_id}")
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
data = json.loads(token_data)
|
||||
if data.get("user_id") != user_id:
|
||||
logger.warning(f"Refresh token user mismatch: {user_id}")
|
||||
raise ValueError("Token user mismatch")
|
||||
|
||||
# Create new access token
|
||||
access_token = jwt_manager.create_access_token(user_id=user_id)
|
||||
|
||||
logger.info(f"Access token refreshed for user_id={user_id}")
|
||||
|
||||
return access_token
|
||||
|
||||
async def authenticate_telegram_user(self, chat_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get JWT token for Telegram user (bot authentication).
|
||||
|
||||
**Flow:**
|
||||
1. Bot queries: GET /auth/telegram/authenticate?chat_id=12345
|
||||
2. Service finds user by telegram_id
|
||||
3. Service creates/returns JWT
|
||||
4. Bot stores JWT for API calls
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
} or None if not found
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(telegram_id=chat_id).first()
|
||||
|
||||
if not user:
|
||||
logger.warning(f"Telegram user not found: {chat_id}")
|
||||
return None
|
||||
|
||||
# Create JWT token
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
logger.info(f"Telegram user authenticated: chat_id={chat_id}, user_id={user.id}")
|
||||
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
255
.history/app/services/auth_service_20251210221406.py
Normal file
255
.history/app/services/auth_service_20251210221406.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Authentication Service - User login, token management, Telegram binding
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
import secrets
|
||||
import json
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.models.user import User
|
||||
from app.security.jwt_manager import jwt_manager
|
||||
from app.core.config import settings
|
||||
import logging
|
||||
import redis
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Redis connection for caching binding codes
|
||||
redis_client = redis.from_url(settings.redis_url)
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""Handles user authentication, token management, and Telegram binding"""
|
||||
|
||||
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. Bot calls /auth/telegram/start with chat_id
|
||||
2. Service generates random code and stores in Redis
|
||||
3. Bot builds link: https://bot.example.com/bind?code=XXX
|
||||
4. Bot sends link to user in Telegram
|
||||
5. User clicks link, authenticates, calls /auth/telegram/confirm
|
||||
"""
|
||||
code = secrets.token_urlsafe(self.BINDING_CODE_LENGTH)
|
||||
|
||||
# Store in Redis with TTL (10 minutes)
|
||||
cache_key = f"binding_code:{code}"
|
||||
cache_data = {
|
||||
"chat_id": chat_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
redis_client.setex(
|
||||
cache_key,
|
||||
self.TELEGRAM_BINDING_CODE_TTL,
|
||||
json.dumps(cache_data),
|
||||
)
|
||||
|
||||
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 after user clicks link.
|
||||
|
||||
**Flow:**
|
||||
1. User authenticates with email/password
|
||||
2. User clicks binding link: /bind?code=XXX
|
||||
3. Frontend calls /auth/telegram/confirm with code
|
||||
4. Service validates code from Redis
|
||||
5. Service links user.id with telegram_id
|
||||
6. Service returns JWT for bot to use
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"success": True,
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
}
|
||||
"""
|
||||
|
||||
# Validate code from Redis
|
||||
cache_key = f"binding_code:{code}"
|
||||
cached_data = redis_client.get(cache_key)
|
||||
|
||||
if not cached_data:
|
||||
logger.warning(f"Binding code not found or expired: {code}")
|
||||
return {"success": False, "error": "Code expired"}
|
||||
|
||||
binding_data = json.loads(cached_data)
|
||||
cached_chat_id = binding_data.get("chat_id")
|
||||
|
||||
if cached_chat_id != chat_id:
|
||||
logger.warning(
|
||||
f"Chat ID mismatch: expected {cached_chat_id}, got {chat_id}"
|
||||
)
|
||||
return {"success": False, "error": "Code mismatch"}
|
||||
|
||||
# Get user from database
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
logger.error(f"User not found: {user_id}")
|
||||
return {"success": False, "error": "User not found"}
|
||||
|
||||
# Update user with Telegram info
|
||||
user.telegram_id = chat_id
|
||||
if username:
|
||||
user.username = username
|
||||
if first_name:
|
||||
user.first_name = first_name
|
||||
if last_name:
|
||||
user.last_name = last_name
|
||||
|
||||
user.updated_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
# Create JWT token for bot
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
# Remove code from Redis
|
||||
redis_client.delete(cache_key)
|
||||
|
||||
logger.info(
|
||||
f"Telegram binding confirmed: user_id={user_id}, chat_id={chat_id}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
|
||||
async def create_session(
|
||||
self,
|
||||
user_id: int,
|
||||
device_id: Optional[str] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Create session and issue tokens.
|
||||
|
||||
**Returns:**
|
||||
(access_token, refresh_token)
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# Create tokens
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
refresh_token = jwt_manager.create_refresh_token(user_id=user.id)
|
||||
|
||||
# Store refresh token in Redis for validation
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = {
|
||||
"user_id": user.id,
|
||||
"device_id": device_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Refresh token valid for 30 days
|
||||
redis_client.setex(
|
||||
token_key,
|
||||
86400 * 30,
|
||||
json.dumps(token_data),
|
||||
)
|
||||
|
||||
# Update user activity
|
||||
user.last_activity = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
logger.info(f"Session created for user_id={user_id}")
|
||||
|
||||
return access_token, refresh_token
|
||||
|
||||
async def refresh_access_token(
|
||||
self,
|
||||
refresh_token: str,
|
||||
user_id: int,
|
||||
) -> str:
|
||||
"""
|
||||
Issue new access token using valid refresh token.
|
||||
|
||||
**Flow:**
|
||||
1. Check refresh token in Redis
|
||||
2. Validate it belongs to user_id
|
||||
3. Create new access token
|
||||
4. Return new token (don't invalidate refresh token)
|
||||
"""
|
||||
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = redis_client.get(token_key)
|
||||
|
||||
if not token_data:
|
||||
logger.warning(f"Refresh token not found: {user_id}")
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
data = json.loads(token_data)
|
||||
if data.get("user_id") != user_id:
|
||||
logger.warning(f"Refresh token user mismatch: {user_id}")
|
||||
raise ValueError("Token user mismatch")
|
||||
|
||||
# Create new access token
|
||||
access_token = jwt_manager.create_access_token(user_id=user_id)
|
||||
|
||||
logger.info(f"Access token refreshed for user_id={user_id}")
|
||||
|
||||
return access_token
|
||||
|
||||
async def authenticate_telegram_user(self, chat_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get JWT token for Telegram user (bot authentication).
|
||||
|
||||
**Flow:**
|
||||
1. Bot queries: GET /auth/telegram/authenticate?chat_id=12345
|
||||
2. Service finds user by telegram_id
|
||||
3. Service creates/returns JWT
|
||||
4. Bot stores JWT for API calls
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
} or None if not found
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(telegram_id=chat_id).first()
|
||||
|
||||
if not user:
|
||||
logger.warning(f"Telegram user not found: {chat_id}")
|
||||
return None
|
||||
|
||||
# Create JWT token
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
logger.info(f"Telegram user authenticated: chat_id={chat_id}, user_id={user.id}")
|
||||
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
279
.history/app/services/auth_service_20251210221434.py
Normal file
279
.history/app/services/auth_service_20251210221434.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Authentication Service - User login, token management, Telegram binding
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
import secrets
|
||||
import json
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.models.user import User
|
||||
from app.security.jwt_manager import jwt_manager
|
||||
from app.core.config import settings
|
||||
import logging
|
||||
import redis
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Redis connection for caching binding codes
|
||||
redis_client = redis.from_url(settings.redis_url)
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""Handles user authentication, token management, and Telegram binding"""
|
||||
|
||||
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. Bot calls /auth/telegram/start with chat_id
|
||||
2. Service generates random code and stores in Redis
|
||||
3. Bot builds link: https://bot.example.com/bind?code=XXX
|
||||
4. Bot sends link to user in Telegram
|
||||
5. User clicks link, authenticates, calls /auth/telegram/confirm
|
||||
"""
|
||||
code = secrets.token_urlsafe(self.BINDING_CODE_LENGTH)
|
||||
|
||||
# Store in Redis with TTL (10 minutes)
|
||||
cache_key = f"binding_code:{code}"
|
||||
cache_data = {
|
||||
"chat_id": chat_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
redis_client.setex(
|
||||
cache_key,
|
||||
self.TELEGRAM_BINDING_CODE_TTL,
|
||||
json.dumps(cache_data),
|
||||
)
|
||||
|
||||
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 after user clicks link.
|
||||
|
||||
**Flow:**
|
||||
1. User authenticates with email/password
|
||||
2. User clicks binding link: /bind?code=XXX
|
||||
3. Frontend calls /auth/telegram/confirm with code
|
||||
4. Service validates code from Redis
|
||||
5. Service links user.id with telegram_id
|
||||
6. Service returns JWT for bot to use
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"success": True,
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
}
|
||||
|
||||
**Errors:**
|
||||
- Code expired or not found
|
||||
- Code chat_id mismatch
|
||||
- User not found
|
||||
- Binding already exists (user has different telegram_id)
|
||||
"""
|
||||
|
||||
# Validate code from Redis
|
||||
cache_key = f"binding_code:{code}"
|
||||
cached_data = redis_client.get(cache_key)
|
||||
|
||||
if not cached_data:
|
||||
logger.warning(f"Binding code not found or expired: {code}")
|
||||
return {"success": False, "error": "Code expired or invalid"}
|
||||
|
||||
binding_data = json.loads(cached_data)
|
||||
cached_chat_id = binding_data.get("chat_id")
|
||||
|
||||
if cached_chat_id != chat_id:
|
||||
logger.warning(
|
||||
f"Chat ID mismatch: expected {cached_chat_id}, got {chat_id}"
|
||||
)
|
||||
return {"success": False, "error": "Code mismatch"}
|
||||
|
||||
# Get user from database
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
logger.error(f"User not found: {user_id}")
|
||||
return {"success": False, "error": "User not found"}
|
||||
|
||||
# Check if binding already exists (user already bound to different telegram)
|
||||
if user.telegram_id and user.telegram_id != chat_id:
|
||||
logger.warning(
|
||||
f"User {user_id} already bound to telegram_id={user.telegram_id}, "
|
||||
f"attempting to bind to {chat_id}"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Account already bound to different Telegram account (chat_id={user.telegram_id})"
|
||||
}
|
||||
|
||||
# Update user with Telegram info
|
||||
user.telegram_id = chat_id
|
||||
if username:
|
||||
user.username = username
|
||||
if first_name:
|
||||
user.first_name = first_name
|
||||
if last_name:
|
||||
user.last_name = last_name
|
||||
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
try:
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Database error during binding confirmation: {e}")
|
||||
return {"success": False, "error": "Database error"}
|
||||
|
||||
# Create JWT token for bot
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
# Remove code from Redis
|
||||
redis_client.delete(cache_key)
|
||||
|
||||
logger.info(
|
||||
f"Telegram binding confirmed: user_id={user_id}, chat_id={chat_id}, "
|
||||
f"username={username}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
|
||||
async def create_session(
|
||||
self,
|
||||
user_id: int,
|
||||
device_id: Optional[str] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Create session and issue tokens.
|
||||
|
||||
**Returns:**
|
||||
(access_token, refresh_token)
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# Create tokens
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
refresh_token = jwt_manager.create_refresh_token(user_id=user.id)
|
||||
|
||||
# Store refresh token in Redis for validation
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = {
|
||||
"user_id": user.id,
|
||||
"device_id": device_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Refresh token valid for 30 days
|
||||
redis_client.setex(
|
||||
token_key,
|
||||
86400 * 30,
|
||||
json.dumps(token_data),
|
||||
)
|
||||
|
||||
# Update user activity
|
||||
user.last_activity = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
logger.info(f"Session created for user_id={user_id}")
|
||||
|
||||
return access_token, refresh_token
|
||||
|
||||
async def refresh_access_token(
|
||||
self,
|
||||
refresh_token: str,
|
||||
user_id: int,
|
||||
) -> str:
|
||||
"""
|
||||
Issue new access token using valid refresh token.
|
||||
|
||||
**Flow:**
|
||||
1. Check refresh token in Redis
|
||||
2. Validate it belongs to user_id
|
||||
3. Create new access token
|
||||
4. Return new token (don't invalidate refresh token)
|
||||
"""
|
||||
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = redis_client.get(token_key)
|
||||
|
||||
if not token_data:
|
||||
logger.warning(f"Refresh token not found: {user_id}")
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
data = json.loads(token_data)
|
||||
if data.get("user_id") != user_id:
|
||||
logger.warning(f"Refresh token user mismatch: {user_id}")
|
||||
raise ValueError("Token user mismatch")
|
||||
|
||||
# Create new access token
|
||||
access_token = jwt_manager.create_access_token(user_id=user_id)
|
||||
|
||||
logger.info(f"Access token refreshed for user_id={user_id}")
|
||||
|
||||
return access_token
|
||||
|
||||
async def authenticate_telegram_user(self, chat_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get JWT token for Telegram user (bot authentication).
|
||||
|
||||
**Flow:**
|
||||
1. Bot queries: GET /auth/telegram/authenticate?chat_id=12345
|
||||
2. Service finds user by telegram_id
|
||||
3. Service creates/returns JWT
|
||||
4. Bot stores JWT for API calls
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
} or None if not found
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(telegram_id=chat_id).first()
|
||||
|
||||
if not user:
|
||||
logger.warning(f"Telegram user not found: {chat_id}")
|
||||
return None
|
||||
|
||||
# Create JWT token
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
logger.info(f"Telegram user authenticated: chat_id={chat_id}, user_id={user.id}")
|
||||
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
279
.history/app/services/auth_service_20251210221526.py
Normal file
279
.history/app/services/auth_service_20251210221526.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Authentication Service - User login, token management, Telegram binding
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
import secrets
|
||||
import json
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.models.user import User
|
||||
from app.security.jwt_manager import jwt_manager
|
||||
from app.core.config import settings
|
||||
import logging
|
||||
import redis
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Redis connection for caching binding codes
|
||||
redis_client = redis.from_url(settings.redis_url)
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""Handles user authentication, token management, and Telegram binding"""
|
||||
|
||||
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. Bot calls /auth/telegram/start with chat_id
|
||||
2. Service generates random code and stores in Redis
|
||||
3. Bot builds link: https://bot.example.com/bind?code=XXX
|
||||
4. Bot sends link to user in Telegram
|
||||
5. User clicks link, authenticates, calls /auth/telegram/confirm
|
||||
"""
|
||||
code = secrets.token_urlsafe(self.BINDING_CODE_LENGTH)
|
||||
|
||||
# Store in Redis with TTL (10 minutes)
|
||||
cache_key = f"binding_code:{code}"
|
||||
cache_data = {
|
||||
"chat_id": chat_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
redis_client.setex(
|
||||
cache_key,
|
||||
self.TELEGRAM_BINDING_CODE_TTL,
|
||||
json.dumps(cache_data),
|
||||
)
|
||||
|
||||
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 after user clicks link.
|
||||
|
||||
**Flow:**
|
||||
1. User authenticates with email/password
|
||||
2. User clicks binding link: /bind?code=XXX
|
||||
3. Frontend calls /auth/telegram/confirm with code
|
||||
4. Service validates code from Redis
|
||||
5. Service links user.id with telegram_id
|
||||
6. Service returns JWT for bot to use
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"success": True,
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
}
|
||||
|
||||
**Errors:**
|
||||
- Code expired or not found
|
||||
- Code chat_id mismatch
|
||||
- User not found
|
||||
- Binding already exists (user has different telegram_id)
|
||||
"""
|
||||
|
||||
# Validate code from Redis
|
||||
cache_key = f"binding_code:{code}"
|
||||
cached_data = redis_client.get(cache_key)
|
||||
|
||||
if not cached_data:
|
||||
logger.warning(f"Binding code not found or expired: {code}")
|
||||
return {"success": False, "error": "Code expired or invalid"}
|
||||
|
||||
binding_data = json.loads(cached_data)
|
||||
cached_chat_id = binding_data.get("chat_id")
|
||||
|
||||
if cached_chat_id != chat_id:
|
||||
logger.warning(
|
||||
f"Chat ID mismatch: expected {cached_chat_id}, got {chat_id}"
|
||||
)
|
||||
return {"success": False, "error": "Code mismatch"}
|
||||
|
||||
# Get user from database
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
logger.error(f"User not found: {user_id}")
|
||||
return {"success": False, "error": "User not found"}
|
||||
|
||||
# Check if binding already exists (user already bound to different telegram)
|
||||
if user.telegram_id and user.telegram_id != chat_id:
|
||||
logger.warning(
|
||||
f"User {user_id} already bound to telegram_id={user.telegram_id}, "
|
||||
f"attempting to bind to {chat_id}"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Account already bound to different Telegram account (chat_id={user.telegram_id})"
|
||||
}
|
||||
|
||||
# Update user with Telegram info
|
||||
user.telegram_id = chat_id
|
||||
if username:
|
||||
user.username = username
|
||||
if first_name:
|
||||
user.first_name = first_name
|
||||
if last_name:
|
||||
user.last_name = last_name
|
||||
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
try:
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Database error during binding confirmation: {e}")
|
||||
return {"success": False, "error": "Database error"}
|
||||
|
||||
# Create JWT token for bot
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
# Remove code from Redis
|
||||
redis_client.delete(cache_key)
|
||||
|
||||
logger.info(
|
||||
f"Telegram binding confirmed: user_id={user_id}, chat_id={chat_id}, "
|
||||
f"username={username}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
|
||||
async def create_session(
|
||||
self,
|
||||
user_id: int,
|
||||
device_id: Optional[str] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Create session and issue tokens.
|
||||
|
||||
**Returns:**
|
||||
(access_token, refresh_token)
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# Create tokens
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
refresh_token = jwt_manager.create_refresh_token(user_id=user.id)
|
||||
|
||||
# Store refresh token in Redis for validation
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = {
|
||||
"user_id": user.id,
|
||||
"device_id": device_id,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Refresh token valid for 30 days
|
||||
redis_client.setex(
|
||||
token_key,
|
||||
86400 * 30,
|
||||
json.dumps(token_data),
|
||||
)
|
||||
|
||||
# Update user activity
|
||||
user.last_activity = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
logger.info(f"Session created for user_id={user_id}")
|
||||
|
||||
return access_token, refresh_token
|
||||
|
||||
async def refresh_access_token(
|
||||
self,
|
||||
refresh_token: str,
|
||||
user_id: int,
|
||||
) -> str:
|
||||
"""
|
||||
Issue new access token using valid refresh token.
|
||||
|
||||
**Flow:**
|
||||
1. Check refresh token in Redis
|
||||
2. Validate it belongs to user_id
|
||||
3. Create new access token
|
||||
4. Return new token (don't invalidate refresh token)
|
||||
"""
|
||||
|
||||
token_key = f"refresh_token:{refresh_token}"
|
||||
token_data = redis_client.get(token_key)
|
||||
|
||||
if not token_data:
|
||||
logger.warning(f"Refresh token not found: {user_id}")
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
data = json.loads(token_data)
|
||||
if data.get("user_id") != user_id:
|
||||
logger.warning(f"Refresh token user mismatch: {user_id}")
|
||||
raise ValueError("Token user mismatch")
|
||||
|
||||
# Create new access token
|
||||
access_token = jwt_manager.create_access_token(user_id=user_id)
|
||||
|
||||
logger.info(f"Access token refreshed for user_id={user_id}")
|
||||
|
||||
return access_token
|
||||
|
||||
async def authenticate_telegram_user(self, chat_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get JWT token for Telegram user (bot authentication).
|
||||
|
||||
**Flow:**
|
||||
1. Bot queries: GET /auth/telegram/authenticate?chat_id=12345
|
||||
2. Service finds user by telegram_id
|
||||
3. Service creates/returns JWT
|
||||
4. Bot stores JWT for API calls
|
||||
|
||||
**Returns:**
|
||||
{
|
||||
"user_id": 123,
|
||||
"jwt_token": "eyJ...",
|
||||
"expires_at": "2025-12-11T12:00:00",
|
||||
} or None if not found
|
||||
"""
|
||||
|
||||
user = self.db.query(User).filter_by(telegram_id=chat_id).first()
|
||||
|
||||
if not user:
|
||||
logger.warning(f"Telegram user not found: {chat_id}")
|
||||
return None
|
||||
|
||||
# Create JWT token
|
||||
access_token = jwt_manager.create_access_token(user_id=user.id)
|
||||
|
||||
logger.info(f"Telegram user authenticated: chat_id={chat_id}, user_id={user.id}")
|
||||
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"jwt_token": access_token,
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(hours=24)
|
||||
).isoformat(),
|
||||
}
|
||||
Reference in New Issue
Block a user