Files
bakery-ia/services/auth/app/services/user_service.py
2026-01-16 15:19:34 +01:00

526 lines
21 KiB
Python

"""
Enhanced User Service
Updated to use repository pattern with dependency injection and improved error handling
"""
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import HTTPException, status
import structlog
from app.repositories import UserRepository, TokenRepository
from app.schemas.auth import UserResponse
from app.schemas.users import UserUpdate
from app.models.users import User
from app.models.tokens import RefreshToken
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()
class EnhancedUserService:
"""Enhanced user management service using repository pattern"""
def __init__(self, database_manager):
"""Initialize service with database manager"""
self.database_manager = database_manager
async def get_user_by_id(self, user_id: str, session: Optional[AsyncSession] = None) -> Optional[UserResponse]:
"""Get user by ID using repository pattern"""
try:
if session:
# Use provided session (for direct session injection)
user_repo = UserRepository(User, session)
user = await user_repo.get_by_id(user_id)
else:
# Use database manager to get session
async with self.database_manager.get_session() as db_session:
user_repo = UserRepository(User, db_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,
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 ID using repository pattern",
user_id=user_id,
error=str(e))
raise DatabaseError(f"Failed to get user: {str(e)}")
async def get_user_by_email(self, email: str) -> Optional[UserResponse]:
"""Get user by email using repository pattern"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(User, 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(User, 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(User, 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("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(User, 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(User, 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(User, 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(User, 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(User, session)
# Validate role
valid_roles = ["user", "admin", "manager", "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 update_user_field(
self,
user_id: str,
field_name: str,
field_value: Any
) -> bool:
"""Update a single field on a user record"""
try:
async with self.database_manager.get_session() as session:
user_repo = UserRepository(User, session)
# Update the specific field
updated_user = await user_repo.update(user_id, {field_name: field_value})
if not updated_user:
logger.error("User not found for field update",
user_id=user_id,
field_name=field_name)
return False
await session.commit()
logger.info("User field updated",
user_id=user_id,
field_name=field_name)
return True
except Exception as e:
logger.error("Failed to update user field",
user_id=user_id,
field_name=field_name,
error=str(e))
return False
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(User, session)
token_repo = TokenRepository(RefreshToken, 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_active_tokens_for_user(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)}