REFACTOR - Database logic

This commit is contained in:
Urtzi Alfaro
2025-08-08 09:08:41 +02:00
parent 0154365bfc
commit 488bb3ef93
113 changed files with 22842 additions and 6503 deletions

View File

@@ -0,0 +1,30 @@
"""
Auth Service Layer
Business logic services for authentication and user management
"""
from .auth_service import AuthService
from .auth_service import EnhancedAuthService
from .user_service import UserService
from .auth_service import EnhancedUserService
from .auth_service_clients import AuthServiceClientFactory
from .admin_delete import AdminUserDeleteService
from .messaging import (
publish_user_registered,
publish_user_login,
publish_user_updated,
publish_user_deactivated
)
__all__ = [
"AuthService",
"EnhancedAuthService",
"UserService",
"EnhancedUserService",
"AuthServiceClientFactory",
"AdminUserDeleteService",
"publish_user_registered",
"publish_user_login",
"publish_user_updated",
"publish_user_deactivated"
]

View File

@@ -1,310 +1,284 @@
# services/auth/app/services/auth_service.py - UPDATED WITH NEW REGISTRATION METHOD
"""
Authentication Service - Updated to support registration with direct token issuance
Enhanced Authentication Service
Updated to use repository pattern with dependency injection and improved error handling
"""
import hashlib
import uuid
from datetime import datetime, timedelta, timezone
from typing import Dict, Any, Optional
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from sqlalchemy.exc import IntegrityError
import structlog
from app.repositories import UserRepository, TokenRepository
from app.schemas.auth import UserRegistration, UserLogin, TokenResponse, UserResponse
from app.models.users import User, RefreshToken
from app.schemas.auth import UserRegistration, UserLogin
from app.core.security import SecurityManager
from app.services.messaging import publish_user_registered, publish_user_login
from shared.database.unit_of_work import UnitOfWork
from shared.database.transactions import transactional
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
logger = structlog.get_logger()
class AuthService:
"""Enhanced Authentication service with unified token response"""
@staticmethod
async def register_user(user_data: UserRegistration, db: AsyncSession) -> Dict[str, Any]:
"""Register a new user with FIXED token generation"""
# Legacy compatibility alias
AuthService = None # Will be set at the end of the file
class EnhancedAuthService:
"""Enhanced authentication service using repository pattern"""
def __init__(self, database_manager):
"""Initialize service with database manager"""
self.database_manager = database_manager
async def register_user(
self,
user_data: UserRegistration
) -> TokenResponse:
"""Register a new user using repository pattern"""
try:
# Check if user already exists
existing_user = await db.execute(
select(User).where(User.email == user_data.email)
)
if existing_user.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User with this email already exists"
)
user_role = user_data.role if user_data.role else "user"
# Create new user
hashed_password = SecurityManager.hash_password(user_data.password)
new_user = User(
id=uuid.uuid4(),
email=user_data.email,
full_name=user_data.full_name,
hashed_password=hashed_password,
is_active=True,
is_verified=False,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
role=user_role
)
db.add(new_user)
await db.flush() # Get user ID without committing
logger.debug(f"User created with role: {new_user.role} for {user_data.email}")
# ✅ FIX 1: Create SEPARATE access and refresh tokens with different payloads
access_token_data = {
"user_id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"is_verified": new_user.is_verified,
"is_active": new_user.is_active,
"role": new_user.role,
"type": "access" # ✅ Explicitly mark as access token
}
refresh_token_data = {
"user_id": str(new_user.id),
"email": new_user.email,
"type": "refresh" # ✅ Explicitly mark as refresh token
}
logger.debug(f"Creating tokens for registration: {user_data.email}")
# ✅ FIX 2: Generate tokens with different payloads
access_token = SecurityManager.create_access_token(user_data=access_token_data)
refresh_token_value = SecurityManager.create_refresh_token(user_data=refresh_token_data)
logger.debug(f"Tokens created successfully for {user_data.email}")
# ✅ FIX 3: Store ONLY the refresh token in database (not access token)
refresh_token = RefreshToken(
id=uuid.uuid4(),
user_id=new_user.id,
token=refresh_token_value, # Store the actual refresh token
expires_at=datetime.now(timezone.utc) + timedelta(days=30),
is_revoked=False,
created_at=datetime.now(timezone.utc)
)
db.add(refresh_token)
await db.commit()
# Publish registration event (non-blocking)
try:
await publish_user_registered({
"user_id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"role": new_user.role,
"registered_at": datetime.now(timezone.utc).isoformat()
})
except Exception as e:
logger.warning(f"Failed to publish registration event: {e}")
logger.info(f"User registered successfully: {user_data.email}")
return {
"access_token": access_token,
"refresh_token": refresh_token_value,
"token_type": "bearer",
"expires_in": 1800, # 30 minutes
"user": {
"id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"is_active": new_user.is_active,
"is_verified": new_user.is_verified,
"created_at": new_user.created_at.isoformat(),
"role": new_user.role
}
}
except HTTPException:
await db.rollback()
async with self.database_manager.get_session() as db_session:
async with UnitOfWork(db_session) as uow:
# Register repositories
user_repo = uow.register_repository("users", UserRepository, User)
token_repo = uow.register_repository("tokens", TokenRepository, RefreshToken)
# Check if user already exists
existing_user = await user_repo.get_by_email(user_data.email)
if existing_user:
raise DuplicateRecordError("User with this email already exists")
# Create user data
user_role = user_data.role if user_data.role else "user"
hashed_password = SecurityManager.hash_password(user_data.password)
create_data = {
"email": user_data.email,
"full_name": user_data.full_name,
"hashed_password": hashed_password,
"is_active": True,
"is_verified": False,
"role": user_role
}
# Create user using repository
new_user = await user_repo.create_user(create_data)
logger.debug("User created with repository pattern",
user_id=new_user.id,
email=user_data.email,
role=user_role)
# Create tokens with different payloads
access_token_data = {
"user_id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"is_verified": new_user.is_verified,
"is_active": new_user.is_active,
"role": new_user.role,
"type": "access"
}
refresh_token_data = {
"user_id": str(new_user.id),
"email": new_user.email,
"type": "refresh"
}
# Generate tokens
access_token = SecurityManager.create_access_token(user_data=access_token_data)
refresh_token_value = SecurityManager.create_refresh_token(user_data=refresh_token_data)
# Store refresh token using repository
token_data = {
"user_id": str(new_user.id),
"token": refresh_token_value,
"expires_at": datetime.now(timezone.utc) + timedelta(days=30),
"is_revoked": False
}
await token_repo.create_token(token_data)
# Commit transaction
await uow.commit()
# Publish registration event (non-blocking)
try:
await publish_user_registered({
"user_id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"role": new_user.role,
"registered_at": datetime.now(timezone.utc).isoformat()
})
except Exception as e:
logger.warning("Failed to publish registration event", error=str(e))
logger.info("User registered successfully using repository pattern",
user_id=new_user.id,
email=user_data.email)
from app.schemas.auth import UserData
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token_value,
token_type="bearer",
expires_in=1800,
user=UserData(
id=str(new_user.id),
email=new_user.email,
full_name=new_user.full_name,
is_active=new_user.is_active,
is_verified=new_user.is_verified,
created_at=new_user.created_at.isoformat() if new_user.created_at else datetime.now(timezone.utc).isoformat(),
role=new_user.role
)
)
except (ValidationError, DuplicateRecordError):
raise
except IntegrityError as e:
await db.rollback()
logger.error(f"Registration failed for {user_data.email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration failed"
)
except Exception as e:
await db.rollback()
logger.error(f"Registration failed for {user_data.email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration failed"
)
@staticmethod
async def login_user(login_data: UserLogin, db: AsyncSession) -> Dict[str, Any]:
"""Login user with FIXED token generation and SQLAlchemy syntax"""
logger.error("Registration failed using repository pattern",
email=user_data.email,
error=str(e))
raise DatabaseError(f"Registration failed: {str(e)}")
async def login_user(
self,
login_data: UserLogin
) -> TokenResponse:
"""Login user using repository pattern"""
try:
# Find user
result = await db.execute(
select(User).where(User.email == login_data.email)
)
user = result.scalar_one_or_none()
if not user or not SecurityManager.verify_password(login_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is deactivated"
)
# ✅ FIX 4: Revoke existing refresh tokens using proper SQLAlchemy ORM syntax
logger.debug(f"Revoking existing refresh tokens for user: {user.id}")
# Using SQLAlchemy ORM update (more reliable than raw SQL)
stmt = update(RefreshToken).where(
RefreshToken.user_id == user.id,
RefreshToken.is_revoked == False
).values(
is_revoked=True,
revoked_at=datetime.now(timezone.utc)
)
result = await db.execute(stmt)
revoked_count = result.rowcount
logger.debug(f"Revoked {revoked_count} existing refresh tokens for user: {user.id}")
# ✅ FIX 5: Create DIFFERENT token payloads
access_token_data = {
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"is_verified": user.is_verified,
"is_active": user.is_active,
"role": user.role,
"type": "access" # ✅ Explicitly mark as access token
}
refresh_token_data = {
"user_id": str(user.id),
"email": user.email,
"type": "refresh", # ✅ Explicitly mark as refresh token
"jti": str(uuid.uuid4()) # ✅ Add unique identifier for each refresh token
}
logger.debug(f"Creating access token for login with data: {list(access_token_data.keys())}")
logger.debug(f"Creating refresh token for login with data: {list(refresh_token_data.keys())}")
# ✅ FIX 6: Generate tokens with different payloads and expiration
access_token = SecurityManager.create_access_token(user_data=access_token_data)
refresh_token_value = SecurityManager.create_refresh_token(user_data=refresh_token_data)
logger.debug(f"Access token created successfully for user {login_data.email}")
logger.debug(f"Refresh token created successfully for user {str(user.id)}")
# ✅ FIX 7: Store ONLY refresh token in database with unique constraint handling
refresh_token = RefreshToken(
id=uuid.uuid4(),
user_id=user.id,
token=refresh_token_value, # This should be the refresh token, not access token
expires_at=datetime.now(timezone.utc) + timedelta(days=30),
is_revoked=False,
created_at=datetime.now(timezone.utc)
)
db.add(refresh_token)
# Update last login
user.last_login = datetime.now(timezone.utc)
await db.commit()
# Publish login event (non-blocking)
try:
await publish_user_login({
"user_id": str(user.id),
"email": user.email,
"login_at": datetime.now(timezone.utc).isoformat()
})
except Exception as e:
logger.warning(f"Failed to publish login event: {e}")
logger.info(f"User logged in successfully: {login_data.email}")
return {
"access_token": access_token,
"refresh_token": refresh_token_value,
"token_type": "bearer",
"expires_in": 1800, # 30 minutes
"user": {
"id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"is_active": user.is_active,
"is_verified": user.is_verified,
"created_at": user.created_at.isoformat(),
"role": user.role
}
}
async with self.database_manager.get_session() as db_session:
async with UnitOfWork(db_session) as uow:
# Register repositories
user_repo = uow.register_repository("users", UserRepository, User)
token_repo = uow.register_repository("tokens", TokenRepository, RefreshToken)
# Authenticate user using repository
user = await user_repo.authenticate_user(login_data.email, login_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is deactivated"
)
# Revoke existing refresh tokens using repository
await token_repo.revoke_all_user_tokens(str(user.id))
logger.debug("Existing tokens revoked using repository pattern",
user_id=user.id)
# Create tokens with different payloads
access_token_data = {
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"is_verified": user.is_verified,
"is_active": user.is_active,
"role": user.role,
"type": "access"
}
refresh_token_data = {
"user_id": str(user.id),
"email": user.email,
"type": "refresh",
"jti": str(uuid.uuid4())
}
# Generate tokens
access_token = SecurityManager.create_access_token(user_data=access_token_data)
refresh_token_value = SecurityManager.create_refresh_token(user_data=refresh_token_data)
# Store refresh token using repository
token_data = {
"user_id": str(user.id),
"token": refresh_token_value,
"expires_at": datetime.now(timezone.utc) + timedelta(days=30),
"is_revoked": False
}
await token_repo.create_token(token_data)
# Update last login using repository
await user_repo.update_last_login(str(user.id))
# Commit transaction
await uow.commit()
# Publish login event (non-blocking)
try:
await publish_user_login({
"user_id": str(user.id),
"email": user.email,
"login_at": datetime.now(timezone.utc).isoformat()
})
except Exception as e:
logger.warning("Failed to publish login event", error=str(e))
logger.info("User logged in successfully using repository pattern",
user_id=user.id,
email=login_data.email)
from app.schemas.auth import UserData
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token_value,
token_type="bearer",
expires_in=1800,
user=UserData(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
created_at=user.created_at.isoformat() if user.created_at else datetime.now(timezone.utc).isoformat(),
role=user.role
)
)
except HTTPException:
await db.rollback()
raise
except IntegrityError as e:
await db.rollback()
logger.error(f"Login failed for {login_data.email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Login failed"
)
except Exception as e:
await db.rollback()
logger.error(f"Login failed for {login_data.email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Login failed"
)
@staticmethod
async def logout_user(user_id: str, refresh_token: str, db: AsyncSession) -> bool:
"""Logout user by revoking refresh token"""
logger.error("Login failed using repository pattern",
email=login_data.email,
error=str(e))
raise DatabaseError(f"Login failed: {str(e)}")
async def logout_user(self, user_id: str, refresh_token: str) -> bool:
"""Logout user using repository pattern"""
try:
# Revoke the specific refresh token using ORM
stmt = update(RefreshToken).where(
RefreshToken.user_id == user_id,
RefreshToken.token == refresh_token,
RefreshToken.is_revoked == False
).values(
is_revoked=True,
revoked_at=datetime.now(timezone.utc)
)
result = await db.execute(stmt)
if result.rowcount > 0:
await db.commit()
logger.info(f"User logged out successfully: {user_id}")
return True
return False
async with self.database_manager.get_session() as session:
token_repo = TokenRepository(session)
# Revoke specific refresh token using repository
success = await token_repo.revoke_token(user_id, refresh_token)
if success:
logger.info("User logged out successfully using repository pattern",
user_id=user_id)
return True
return False
except Exception as e:
await db.rollback()
logger.error(f"Logout failed for user {user_id}: {e}")
logger.error("Logout failed using repository pattern",
user_id=user_id,
error=str(e))
return False
@staticmethod
async def refresh_access_token(refresh_token: str, db: AsyncSession) -> Dict[str, Any]:
"""Refresh access token using refresh token"""
async def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
"""Refresh access token using repository pattern"""
try:
# Verify refresh token
payload = SecurityManager.decode_token(refresh_token)
@@ -316,66 +290,59 @@ class AuthService:
detail="Invalid refresh token"
)
# Check if refresh token exists and is valid using ORM
result = await db.execute(
select(RefreshToken).where(
RefreshToken.user_id == user_id,
RefreshToken.token == refresh_token,
RefreshToken.is_revoked == False,
RefreshToken.expires_at > datetime.now(timezone.utc)
)
)
stored_token = result.scalar_one_or_none()
if not stored_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token"
)
# Get user
user_result = await db.execute(
select(User).where(User.id == user_id)
)
user = user_result.scalar_one_or_none()
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive"
)
# Create new access token
access_token_data = {
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"is_verified": user.is_verified,
"is_active": user.is_active,
"role": user.role,
"type": "access"
}
new_access_token = SecurityManager.create_access_token(user_data=access_token_data)
return {
"access_token": new_access_token,
"token_type": "bearer",
"expires_in": 1800
}
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
# Validate refresh token using repository
is_valid = await token_repo.validate_refresh_token(refresh_token, user_id)
if not is_valid:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token"
)
# Get user using repository
user = await user_repo.get_by_id(user_id)
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive"
)
# Create new access token
access_token_data = {
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"is_verified": user.is_verified,
"is_active": user.is_active,
"role": user.role,
"type": "access"
}
new_access_token = SecurityManager.create_access_token(user_data=access_token_data)
logger.debug("Access token refreshed successfully using repository pattern",
user_id=user_id)
return {
"access_token": new_access_token,
"token_type": "bearer",
"expires_in": 1800
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Token refresh failed: {e}")
logger.error("Token refresh failed using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token refresh failed"
)
@staticmethod
async def verify_user_token(token: str) -> Dict[str, Any]:
"""Verify access token and return user info (UNCHANGED)"""
async def verify_user_token(self, token: str) -> Dict[str, Any]:
"""Verify access token and return user info"""
try:
payload = SecurityManager.verify_token(token)
if not payload:
@@ -387,8 +354,173 @@ class AuthService:
return payload
except Exception as e:
logger.error(f"Token verification error: {e}")
logger.error("Token verification error using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
)
async def get_user_profile(self, user_id: str) -> Optional[UserResponse]:
"""Get user profile using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user = await user_repo.get_by_id(user_id)
if not user:
return None
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
created_at=user.created_at,
role=user.role
)
except Exception as e:
logger.error("Failed to get user profile using repository pattern",
user_id=user_id,
error=str(e))
return None
async def update_user_profile(
self,
user_id: str,
update_data: Dict[str, Any]
) -> Optional[UserResponse]:
"""Update user profile using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
updated_user = await user_repo.update(user_id, update_data)
if not updated_user:
return None
logger.info("User profile updated using repository pattern",
user_id=user_id,
updated_fields=list(update_data.keys()))
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,
full_name=updated_user.full_name,
is_active=updated_user.is_active,
is_verified=updated_user.is_verified,
created_at=updated_user.created_at,
role=updated_user.role
)
except Exception as e:
logger.error("Failed to update user profile using repository pattern",
user_id=user_id,
error=str(e))
raise DatabaseError(f"Failed to update profile: {str(e)}")
async def change_password(
self,
user_id: str,
old_password: str,
new_password: str
) -> bool:
"""Change user password using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
# Get user and verify old password
user = await user_repo.get_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
if not SecurityManager.verify_password(old_password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid old password"
)
# Hash new password and update
new_hashed_password = SecurityManager.hash_password(new_password)
await user_repo.update(user_id, {"hashed_password": new_hashed_password})
# Revoke all existing tokens for security
await token_repo.revoke_all_user_tokens(user_id)
logger.info("Password changed successfully using repository pattern",
user_id=user_id)
return True
except HTTPException:
raise
except Exception as e:
logger.error("Failed to change password using repository pattern",
user_id=user_id,
error=str(e))
raise DatabaseError(f"Failed to change password: {str(e)}")
async def verify_user_email(self, user_id: str, verification_token: str) -> bool:
"""Verify user email using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
# In a real implementation, you'd verify the verification_token
# For now, just mark user as verified
updated_user = await user_repo.update(user_id, {"is_verified": True})
if updated_user:
logger.info("User email verified using repository pattern",
user_id=user_id)
return True
return False
except Exception as e:
logger.error("Failed to verify email using repository pattern",
user_id=user_id,
error=str(e))
return False
async def deactivate_user(self, user_id: str, admin_user_id: str) -> bool:
"""Deactivate user account using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
# Update user status
updated_user = await user_repo.update(user_id, {"is_active": False})
if not updated_user:
return False
# Revoke all tokens
await token_repo.revoke_all_user_tokens(user_id)
logger.info("User deactivated using repository pattern",
user_id=user_id,
admin_user_id=admin_user_id)
return True
except Exception as e:
logger.error("Failed to deactivate user using repository pattern",
user_id=user_id,
error=str(e))
return False
# Legacy compatibility - alias EnhancedAuthService as AuthService
AuthService = EnhancedAuthService
class EnhancedUserService(EnhancedAuthService):
"""User service alias for backward compatibility"""
pass

View File

@@ -36,3 +36,11 @@ async def publish_user_login(user_data: dict) -> bool:
async def publish_user_logout(user_data: dict) -> bool:
"""Publish user logout event"""
return await auth_publisher.publish_user_event("logout", user_data)
async def publish_user_updated(user_data: dict) -> bool:
"""Publish user updated event"""
return await auth_publisher.publish_user_event("updated", user_data)
async def publish_user_deactivated(user_data: dict) -> bool:
"""Publish user deactivated event"""
return await auth_publisher.publish_user_event("deactivated", user_data)

View File

@@ -1,153 +1,484 @@
"""
User service for managing user operations
Enhanced User Service
Updated to use repository pattern with dependency injection and improved error handling
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete
from fastapi import HTTPException, status
from passlib.context import CryptContext
import structlog
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional
from fastapi import HTTPException, status
import structlog
from app.models.users import User
from app.core.config import settings
from app.repositories import UserRepository, TokenRepository
from app.schemas.auth import UserResponse, UserUpdate
from app.core.security import SecurityManager
from shared.database.unit_of_work import UnitOfWork
from shared.database.transactions import transactional
from shared.database.exceptions import DatabaseError, ValidationError
logger = structlog.get_logger()
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class UserService:
"""Service for user management operations"""
class EnhancedUserService:
"""Enhanced user management service using repository pattern"""
@staticmethod
async def get_user_by_id(user_id: str, db: AsyncSession) -> User:
"""Get user by ID"""
try:
result = await db.execute(
select(User).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
except Exception as e:
logger.error(f"Error getting user by ID {user_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user"
)
def __init__(self, database_manager):
"""Initialize service with database manager"""
self.database_manager = database_manager
@staticmethod
async def update_user(user_id: str, user_data: dict, db: AsyncSession) -> User:
"""Update user information"""
async def get_user_by_id(self, user_id: str) -> Optional[UserResponse]:
"""Get user by ID using repository pattern"""
try:
# Get current user
user = await UserService.get_user_by_id(user_id, db)
# Update fields
update_data = {}
allowed_fields = ['full_name', 'phone', 'language', 'timezone']
for field in allowed_fields:
if field in user_data:
update_data[field] = user_data[field]
if update_data:
update_data["updated_at"] = datetime.now(timezone.utc)
await db.execute(
update(User)
.where(User.id == user_id)
.values(**update_data)
)
await db.commit()
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
# Refresh user object
await db.refresh(user)
return user
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating user {user_id}: {e}")
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user"
)
@staticmethod
async def change_password(
user_id: str,
current_password: str,
new_password: str,
db: AsyncSession
):
"""Change user password"""
try:
# Get current user
user = await UserService.get_user_by_id(user_id, db)
# Verify current password
if not pwd_context.verify(current_password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect"
user = await user_repo.get_by_id(user_id)
if not user:
return None
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
created_at=user.created_at,
role=user.role,
phone=getattr(user, 'phone', None),
language=getattr(user, 'language', None),
timezone=getattr(user, 'timezone', None)
)
# Hash new password
new_hashed_password = pwd_context.hash(new_password)
# Update password
await db.execute(
update(User)
.where(User.id == user_id)
.values(hashed_password=new_hashed_password, updated_at=datetime.now(timezone.utc))
)
await db.commit()
logger.info(f"Password changed for user {user_id}")
except HTTPException:
raise
except Exception as e:
logger.error(f"Error changing password for user {user_id}: {e}")
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to change password"
)
logger.error("Failed to get user by ID using repository pattern",
user_id=user_id,
error=str(e))
raise DatabaseError(f"Failed to get user: {str(e)}")
@staticmethod
async def delete_user(user_id: str, db: AsyncSession):
"""Delete user account"""
async def get_user_by_email(self, email: str) -> Optional[UserResponse]:
"""Get user by email using repository pattern"""
try:
# Get current user first
user = await UserService.get_user_by_id(user_id, db)
# Soft delete by deactivating
await db.execute(
update(User)
.where(User.id == user_id)
.values(is_active=False)
)
await db.commit()
logger.info(f"User {user_id} deactivated (soft delete)")
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
user = await user_repo.get_by_email(email)
if not user:
return None
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
created_at=user.created_at,
role=user.role,
phone=getattr(user, 'phone', None),
language=getattr(user, 'language', None),
timezone=getattr(user, 'timezone', None)
)
except Exception as e:
logger.error("Failed to get user by email using repository pattern",
email=email,
error=str(e))
raise DatabaseError(f"Failed to get user: {str(e)}")
async def get_users_list(
self,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
role: str = None
) -> List[UserResponse]:
"""Get paginated list of users using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
filters = {}
if active_only:
filters["is_active"] = True
if role:
filters["role"] = role
users = await user_repo.get_multi(
filters=filters,
skip=skip,
limit=limit,
order_by="created_at",
order_desc=True
)
return [
UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
created_at=user.created_at,
role=user.role,
phone=getattr(user, 'phone', None),
language=getattr(user, 'language', None),
timezone=getattr(user, 'timezone', None)
)
for user in users
]
except Exception as e:
logger.error("Failed to get users list using repository pattern", error=str(e))
return []
@transactional
async def update_user(
self,
user_id: str,
user_data: UserUpdate,
session=None
) -> Optional[UserResponse]:
"""Update user information using repository pattern"""
try:
async with self.database_manager.get_session() as db_session:
user_repo = UserRepository(db_session)
# Validate user exists
existing_user = await user_repo.get_by_id(user_id)
if not existing_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Prepare update data
update_data = {}
if user_data.full_name is not None:
update_data["full_name"] = user_data.full_name
if user_data.phone is not None:
update_data["phone"] = user_data.phone
if user_data.language is not None:
update_data["language"] = user_data.language
if user_data.timezone is not None:
update_data["timezone"] = user_data.timezone
if not update_data:
# No updates to apply
return UserResponse(
id=str(existing_user.id),
email=existing_user.email,
full_name=existing_user.full_name,
is_active=existing_user.is_active,
is_verified=existing_user.is_verified,
created_at=existing_user.created_at,
role=existing_user.role
)
# Update user using repository
updated_user = await user_repo.update(user_id, update_data)
if not updated_user:
raise DatabaseError("Failed to update user")
logger.info("User updated successfully using repository pattern",
user_id=user_id,
updated_fields=list(update_data.keys()))
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,
full_name=updated_user.full_name,
is_active=updated_user.is_active,
is_verified=updated_user.is_verified,
created_at=updated_user.created_at,
role=updated_user.role,
phone=getattr(updated_user, 'phone', None),
language=getattr(updated_user, 'language', None),
timezone=getattr(updated_user, 'timezone', None)
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting user {user_id}: {e}")
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete user"
)
logger.error("Failed to update user using repository pattern",
user_id=user_id,
error=str(e))
raise DatabaseError(f"Failed to update user: {str(e)}")
@transactional
async def change_password(
self,
user_id: str,
current_password: str,
new_password: str,
session=None
) -> bool:
"""Change user password using repository pattern"""
try:
async with self.database_manager.get_session() as db_session:
async with UnitOfWork(db_session) as uow:
# Register repositories
user_repo = uow.register_repository("users", UserRepository)
token_repo = uow.register_repository("tokens", TokenRepository)
# Get user and verify current password
user = await user_repo.get_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
if not SecurityManager.verify_password(current_password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect"
)
# Hash new password and update
new_hashed_password = SecurityManager.hash_password(new_password)
await user_repo.update(user_id, {"hashed_password": new_hashed_password})
# Revoke all existing tokens for security
await token_repo.revoke_user_tokens(user_id)
# Commit transaction
await uow.commit()
logger.info("Password changed successfully using repository pattern",
user_id=user_id)
return True
except HTTPException:
raise
except Exception as e:
logger.error("Failed to change password using repository pattern",
user_id=user_id,
error=str(e))
raise DatabaseError(f"Failed to change password: {str(e)}")
@transactional
async def deactivate_user(self, user_id: str, admin_user_id: str, session=None) -> bool:
"""Deactivate user account using repository pattern"""
try:
async with self.database_manager.get_session() as db_session:
async with UnitOfWork(db_session) as uow:
# Register repositories
user_repo = uow.register_repository("users", UserRepository)
token_repo = uow.register_repository("tokens", TokenRepository)
# Verify user exists
user = await user_repo.get_by_id(user_id)
if not user:
return False
# Update user status (soft delete)
updated_user = await user_repo.update(user_id, {"is_active": False})
if not updated_user:
return False
# Revoke all tokens
await token_repo.revoke_user_tokens(user_id)
# Commit transaction
await uow.commit()
logger.info("User deactivated successfully using repository pattern",
user_id=user_id,
admin_user_id=admin_user_id)
return True
except Exception as e:
logger.error("Failed to deactivate user using repository pattern",
user_id=user_id,
error=str(e))
return False
@transactional
async def activate_user(self, user_id: str, admin_user_id: str, session=None) -> bool:
"""Activate user account using repository pattern"""
try:
async with self.database_manager.get_session() as db_session:
user_repo = UserRepository(db_session)
# Update user status
updated_user = await user_repo.update(user_id, {"is_active": True})
if not updated_user:
return False
logger.info("User activated successfully using repository pattern",
user_id=user_id,
admin_user_id=admin_user_id)
return True
except Exception as e:
logger.error("Failed to activate user using repository pattern",
user_id=user_id,
error=str(e))
return False
async def verify_user_email(self, user_id: str, verification_token: str) -> bool:
"""Verify user email using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
# In a real implementation, you'd verify the verification_token
# For now, just mark user as verified
updated_user = await user_repo.update(user_id, {"is_verified": True})
if updated_user:
logger.info("User email verified using repository pattern",
user_id=user_id)
return True
return False
except Exception as e:
logger.error("Failed to verify email using repository pattern",
user_id=user_id,
error=str(e))
return False
async def get_user_statistics(self) -> Dict[str, Any]:
"""Get user statistics using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
# Get basic user statistics
statistics = await user_repo.get_user_statistics()
return statistics
except Exception as e:
logger.error("Failed to get user statistics using repository pattern", error=str(e))
return {
"total_users": 0,
"active_users": 0,
"verified_users": 0,
"users_by_role": {},
"recent_registrations_7d": 0
}
async def search_users(
self,
search_term: str,
role: str = None,
active_only: bool = True,
skip: int = 0,
limit: int = 50
) -> List[UserResponse]:
"""Search users by email or name using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
users = await user_repo.search_users(
search_term, role, active_only, skip, limit
)
return [
UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
created_at=user.created_at,
role=user.role,
phone=getattr(user, 'phone', None),
language=getattr(user, 'language', None),
timezone=getattr(user, 'timezone', None)
)
for user in users
]
except Exception as e:
logger.error("Failed to search users using repository pattern",
search_term=search_term,
error=str(e))
return []
async def update_user_role(
self,
user_id: str,
new_role: str,
admin_user_id: str
) -> Optional[UserResponse]:
"""Update user role using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
# Validate role
valid_roles = ["user", "admin", "super_admin"]
if new_role not in valid_roles:
raise ValidationError(f"Invalid role. Must be one of: {valid_roles}")
# Update user role
updated_user = await user_repo.update(user_id, {"role": new_role})
if not updated_user:
return None
logger.info("User role updated using repository pattern",
user_id=user_id,
new_role=new_role,
admin_user_id=admin_user_id)
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,
full_name=updated_user.full_name,
is_active=updated_user.is_active,
is_verified=updated_user.is_verified,
created_at=updated_user.created_at,
role=updated_user.role,
phone=getattr(updated_user, 'phone', None),
language=getattr(updated_user, 'language', None),
timezone=getattr(updated_user, 'timezone', None)
)
except ValidationError:
raise
except Exception as e:
logger.error("Failed to update user role using repository pattern",
user_id=user_id,
new_role=new_role,
error=str(e))
raise DatabaseError(f"Failed to update role: {str(e)}")
async def get_user_activity(self, user_id: str) -> Dict[str, Any]:
"""Get user activity information using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(session)
token_repo = TokenRepository(session)
# Get user
user = await user_repo.get_by_id(user_id)
if not user:
return {"error": "User not found"}
# Get token activity
active_tokens = await token_repo.get_user_active_tokens(user_id)
return {
"user_id": user_id,
"last_login": user.last_login.isoformat() if user.last_login else None,
"account_created": user.created_at.isoformat(),
"is_active": user.is_active,
"is_verified": user.is_verified,
"active_sessions": len(active_tokens),
"last_activity": max([token.created_at for token in active_tokens]).isoformat() if active_tokens else None
}
except Exception as e:
logger.error("Failed to get user activity using repository pattern",
user_id=user_id,
error=str(e))
return {"error": str(e)}
# Legacy compatibility - alias EnhancedUserService as UserService
UserService = EnhancedUserService