""" User routes for the backend API. Implements the endpoints for user management. """ from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response, Query from typing import List, Dict, Any, Optional from fastapi import status import logging import json # Import interfaces and models import modules.interfaces.interfaceDbAppObjects as interfaceDbAppObjects from modules.security.auth import getCurrentUser, limiter, getCurrentUser # Import the attribute definition and helper functions from modules.datamodels.datamodelUam import User, UserPrivilege from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata # Configure logger logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/users", tags=["Manage Users"], responses={404: {"description": "Not found"}} ) @router.get("/", response_model=PaginatedResponse[User]) @limiter.limit("30/minute") async def get_users( request: Request, mandateId: Optional[str] = Query(None, description="Mandate ID to filter users"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[User]: """ Get users with optional pagination, sorting, and filtering. Query Parameters: - mandateId: Optional mandate ID to filter users - pagination: JSON-encoded PaginationParams object, or None for no pagination Examples: - GET /api/users/ (no pagination - returns all users) - GET /api/users/?pagination={"page":1,"pageSize":10,"sort":[]} """ try: # Parse pagination parameter paginationParams = None if pagination: try: paginationDict = json.loads(pagination) paginationParams = PaginationParams(**paginationDict) if paginationDict else None except (json.JSONDecodeError, ValueError) as e: raise HTTPException( status_code=400, detail=f"Invalid pagination parameter: {str(e)}" ) appInterface = interfaceDbAppObjects.getInterface(currentUser) # If mandateId is provided, use it, otherwise use the current user's mandate targetMandateId = mandateId or currentUser.mandateId # Get users with optional pagination result = appInterface.getUsersByMandate(targetMandateId, pagination=paginationParams) # If pagination was requested, result is PaginatedResult # If no pagination, result is List[User] if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page, pageSize=paginationParams.pageSize, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort, filters=paginationParams.filters ) ) else: return PaginatedResponse( items=result, pagination=None ) except HTTPException: raise except Exception as e: logger.error(f"Error getting users: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to get users: {str(e)}" ) @router.get("/{userId}", response_model=User) @limiter.limit("30/minute") async def get_user( request: Request, userId: str = Path(..., description="ID of the user"), currentUser: User = Depends(getCurrentUser) ) -> User: """Get a specific user by ID""" try: appInterface = interfaceDbAppObjects.getInterface(currentUser) # Get user without filtering by enabled status user = appInterface.getUser(userId) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {userId} not found" ) return user except HTTPException: raise except Exception as e: logger.error(f"Error getting user {userId}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to get user: {str(e)}" ) @router.post("", response_model=User) @limiter.limit("10/minute") async def create_user( request: Request, user_data: User = Body(...), password: Optional[str] = Body(None, embed=True), currentUser: User = Depends(getCurrentUser) ) -> User: """Create a new user""" appInterface = interfaceDbAppObjects.getInterface(currentUser) # Extract fields from User model and call createUser with individual parameters from modules.datamodels.datamodelUam import AuthAuthority newUser = appInterface.createUser( username=user_data.username, password=password, email=user_data.email, fullName=user_data.fullName, language=user_data.language, enabled=user_data.enabled, privilege=user_data.privilege, authenticationAuthority=user_data.authenticationAuthority ) return newUser @router.put("/{userId}", response_model=User) @limiter.limit("10/minute") async def update_user( request: Request, userId: str = Path(..., description="ID of the user to update"), userData: User = Body(...), currentUser: User = Depends(getCurrentUser) ) -> User: """Update an existing user""" appInterface = interfaceDbAppObjects.getInterface(currentUser) # Check if the user exists existingUser = appInterface.getUser(userId) if not existingUser: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {userId} not found" ) # Update user updatedUser = appInterface.updateUser(userId, userData) if not updatedUser: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error updating the user" ) return updatedUser @router.post("/{userId}/reset-password") @limiter.limit("5/minute") async def reset_user_password( request: Request, userId: str = Path(..., description="ID of the user to reset password for"), newPassword: str = Body(..., embed=True), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Reset user password (Admin only)""" try: # Check if current user is admin if currentUser.privilege != UserPrivilege.ADMIN: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only administrators can reset passwords" ) # Get user interface appInterface = interfaceDbAppObjects.getInterface(currentUser) # Get target user target_user = appInterface.getUserById(userId) if not target_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) # Validate password strength if len(newPassword) < 8: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Password must be at least 8 characters long" ) # Reset password success = appInterface.resetUserPassword(userId, newPassword) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to reset password" ) # SECURITY: Automatically revoke all tokens for the user after password reset try: from modules.datamodels.datamodelUam import AuthAuthority revoked_count = appInterface.revokeTokensByUser( userId=userId, authority=None, # Revoke all authorities mandateId=None, # Revoke across all mandates revokedBy=currentUser.id, reason="password_reset" ) logger.info(f"Revoked {revoked_count} tokens for user {userId} after password reset") except Exception as e: logger.error(f"Failed to revoke tokens after password reset for user {userId}: {str(e)}") # Don't fail the password reset if token revocation fails # Log password reset try: from modules.shared.auditLogger import audit_logger audit_logger.logSecurityEvent( userId=str(currentUser.id), mandateId=str(currentUser.mandateId), action="password_reset", details=f"Reset password for user {userId}" ) except Exception: pass return { "message": "Password reset successfully", "user_id": userId } except HTTPException: raise except Exception as e: logger.error(f"Error resetting password: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Password reset failed: {str(e)}" ) @router.post("/change-password") @limiter.limit("5/minute") async def change_password( request: Request, currentPassword: str = Body(..., embed=True), newPassword: str = Body(..., embed=True), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Change current user's password""" try: # Get user interface appInterface = interfaceDbAppObjects.getInterface(currentUser) # Verify current password if not appInterface.verifyPassword(currentPassword, currentUser.passwordHash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Current password is incorrect" ) # Validate new password strength if len(newPassword) < 8: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="New password must be at least 8 characters long" ) # Change password success = appInterface.resetUserPassword(str(currentUser.id), newPassword) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to change password" ) # SECURITY: Automatically revoke all tokens for the user after password change try: from modules.datamodels.datamodelUam import AuthAuthority revoked_count = appInterface.revokeTokensByUser( userId=str(currentUser.id), authority=None, # Revoke all authorities mandateId=None, # Revoke across all mandates revokedBy=currentUser.id, reason="password_change" ) logger.info(f"Revoked {revoked_count} tokens for user {currentUser.id} after password change") except Exception as e: logger.error(f"Failed to revoke tokens after password change for user {currentUser.id}: {str(e)}") # Don't fail the password change if token revocation fails # Log password change try: from modules.shared.auditLogger import audit_logger audit_logger.logSecurityEvent( userId=str(currentUser.id), mandateId=str(currentUser.mandateId), action="password_change", details="User changed their own password" ) except Exception: pass return { "message": "Password changed successfully. Please log in again with your new password." } except HTTPException: raise except Exception as e: logger.error(f"Error changing password: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Password change failed: {str(e)}" ) @router.delete("/{userId}", response_model=Dict[str, Any]) @limiter.limit("10/minute") async def delete_user( request: Request, userId: str = Path(..., description="ID of the user to delete"), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a user""" appInterface = interfaceDbAppObjects.getInterface(currentUser) # Check if the user exists existingUser = appInterface.getUser(userId) if not existingUser: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {userId} not found" ) success = appInterface.deleteUser(userId) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error deleting the user" ) return {"message": f"User with ID {userId} successfully deleted"}