REFACTOR - Database logic
This commit is contained in:
@@ -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"
|
||||
]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user