486 lines
20 KiB
Python
486 lines
20 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 fastapi import HTTPException, status
|
|
import structlog
|
|
|
|
from app.repositories import UserRepository, TokenRepository
|
|
from app.schemas.auth import UserResponse, 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) -> Optional[UserResponse]:
|
|
"""Get user by ID using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as session:
|
|
user_repo = UserRepository(User, 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 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_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 |