from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request from typing import List, Dict, Any, Optional from fastapi import status from datetime import datetime from dataclasses import dataclass import logging import time import traceback # Import auth module from modules.security.auth import getCurrentActiveUser, getUserContext # Import interfaces from modules.interfaces.gatewayInterface import getGatewayInterface from modules.interfaces.gatewayModel import User # Set up logger logger = logging.getLogger(__name__) # Determine all attributes of the model def getModelAttributes(modelClass): return [attr for attr in dir(modelClass) if not callable(getattr(modelClass, attr)) and not attr.startswith('_') and attr not in ('metadata', 'query', 'query_class', 'label', 'field_labels')] # Model attributes for User userAttributes = getModelAttributes(User) @dataclass class AppContext: """Context object for all required connections and user information""" _mandateId: int _userId: int interfaceData: Any # Gateway Interface async def getContext(currentUser: Dict[str, Any]) -> AppContext: """Creates a central context object with all required connections""" _mandateId, _userId = await getUserContext(currentUser) interfaceData = getGatewayInterface(_mandateId, _userId) return AppContext( _mandateId=_mandateId, _userId=_userId, interfaceData=interfaceData ) # Create router for user endpoints router = APIRouter( prefix="/api/users", tags=["Users"], responses={404: {"description": "Not found"}} ) @router.get("", response_model=List[Dict[str, Any]]) async def getUsers(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): """Get all available users (only for Admin/SysAdmin users)""" context = await getContext(currentUser) # Permission check if currentUser.get("privilege") not in ["admin", "sysadmin"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="No permission to access the user list" ) # Admin sees only users of own mandate, SysAdmin sees all if currentUser.get("privilege") == "admin": return context.interfaceData.getUsersByMandate(context._mandateId) else: # sysadmin return context.interfaceData.getAllUsers() @router.post("/register", response_model=Dict[str, Any]) async def registerUser(request: Request): """Register a new user.""" try: # Get request data data = await request.json() logger.info(f"Registration request data: {data}") # Get root mandate and admin user IDs adminGateway = getGatewayInterface() rootMandateId = adminGateway.getInitialId("mandates") adminUserId = adminGateway.getInitialId("users") if not rootMandateId or not adminUserId: raise HTTPException( status_code=500, detail="System is not properly initialized with root mandate and admin user" ) # Create a new gateway interface instance with admin context adminGateway = getGatewayInterface(rootMandateId, adminUserId) # Check required fields if not data.get("username") or not data.get("password"): logger.error("Missing required fields in registration request") raise HTTPException(status_code=400, detail="Username and password are required") # Create user data userData = { "username": data["username"], "password": data["password"], "email": data.get("email"), "fullName": data.get("fullName"), "language": data.get("language", "de"), "_mandateId": rootMandateId, "disabled": False, "privilege": "user" } # Create the user logger.info(f"Attempting to create user with data: {userData}") createdUser = adminGateway.createUser(**userData) logger.info(f"User created successfully: {createdUser}") # Add a small delay to ensure database consistency time.sleep(0.5) # Verify the user was created and password was stored if "hashedPassword" not in createdUser: logger.error("Password not stored in user record") # Try to delete the user try: adminGateway.deleteUser(createdUser["id"]) logger.info("Successfully deleted user after password storage failure") except Exception as e: logger.error(f"Failed to delete user after password storage failure: {str(e)}") raise HTTPException(status_code=500, detail="Password storage failed") logger.info("User verification successful") # Test authentication try: authResult = adminGateway.authenticateUser(userData["username"], userData["password"]) if not authResult: logger.error("Authentication test failed after user creation") # Try to delete the user try: adminGateway.deleteUser(createdUser["id"]) logger.info("Successfully deleted user after authentication test failure") except Exception as e: logger.error(f"Failed to delete user after authentication test failure: {str(e)}") raise HTTPException(status_code=500, detail="Authentication test failed") except ValueError as e: logger.error(f"Authentication test failed: {str(e)}") # Try to delete the user try: adminGateway.deleteUser(createdUser["id"]) logger.info("Successfully deleted user after authentication test failure") except Exception as e: logger.error(f"Failed to delete user after authentication test failure: {str(e)}") raise HTTPException(status_code=500, detail=f"Authentication test failed: {str(e)}") logger.info("Authentication test successful") # Return success response return { "message": "User registered successfully", "userId": createdUser["id"] } except ValueError as e: logger.error(f"Validation error during registration: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) except PermissionError as e: logger.error(f"Permission error during registration: {str(e)}") raise HTTPException(status_code=403, detail=str(e)) except Exception as e: logger.error(f"Unexpected error during registration: {str(e)}") logger.error(traceback.format_exc()) raise HTTPException(status_code=500, detail="Internal server error") @router.post("/register-with-msal", response_model=Dict[str, Any]) async def registerUserWithMsal(userData: dict = Body(...)): """Register a new user using Microsoft authentication""" # Add debug logging import logging logger = logging.getLogger(__name__) logger.info(f"MSAL Registration request data: {userData}") # Get the initial IDs for mandate and admin user adminGateway = getGatewayInterface() # Get ID of the root mandate - we'll use this for new users rootMandateId = adminGateway.getInitialId("mandates") adminUserId = adminGateway.getInitialId("users") if not rootMandateId or not adminUserId: raise HTTPException( status_code=500, detail="System is not properly initialized with root mandate and admin user" ) # Use a gateway with admin context for user creation gateway = getGatewayInterface(rootMandateId, adminUserId) if "username" not in userData: raise HTTPException(status_code=400, detail="Username required") try: # Create user data with a random password since it won't be used import secrets random_password = secrets.token_urlsafe(32) # Create user with required fields newUser = gateway.createUser( username=userData["username"], password=random_password, # Random password since MSAL auth will be used email=userData.get("email"), fullName=userData.get("fullName"), language=userData.get("language", "de"), _mandateId=rootMandateId, disabled=False, privilege="user" ) return newUser except ValueError as e: logger.error(f"ValueError in MSAL registration: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) except PermissionError as e: logger.error(f"PermissionError in MSAL registration: {str(e)}") raise HTTPException(status_code=403, detail=str(e)) except Exception as e: import traceback logger.error(f"Unexpected error in MSAL registration: {str(e)}") logger.error(traceback.format_exc()) raise HTTPException(status_code=500, detail=f"MSAL Registration failed: {str(e)}") @router.get("/{userId}", response_model=Dict[str, Any]) async def getUser( userId: str = Path(..., description="ID of the user"), currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) ): """Get a specific user""" context = await getContext(currentUser) # Initialize gateway interface with user context userToGet = context.interfaceData.getUser(userId) if not userToGet: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {userId} not found" ) # Permission check # User can only view themselves, Admin only users of their own mandate, SysAdmin all if userId == str(context._userId): # User can view themselves pass elif currentUser.get("privilege") == "admin" and userToGet.get("_mandateId") == context._mandateId: # Admin can view users of their own mandate pass elif currentUser.get("privilege") == "sysadmin": # SysAdmin can view all users pass else: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="No permission to view this user" ) return userToGet @router.put("/{userId}", response_model=Dict[str, Any]) async def updateUser( userId: str = Path(..., description="ID of the user to update"), userData: Dict[str, Any] = Body(..., description="Updated user data"), currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) ): """Update an existing user""" context = await getContext(currentUser) # User exists? userToUpdate = context.interfaceData.getUser(userId) if not userToUpdate: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {userId} not found" ) # Permission check isSelfUpdate = userId == str(context._userId) isAdmin = currentUser.get("privilege") == "admin" isSysadmin = currentUser.get("privilege") == "sysadmin" sameMandate = userToUpdate.get("_mandateId") == context._mandateId # Filter allowed fields based on permission level allowedFields = {"username", "email", "fullName", "language"} sensitiveFields = {"_mandateId", "disabled", "privilege"} # Check if sensitive fields should be changed sensitiveUpdate = any(field in userData for field in sensitiveFields) if isSelfUpdate and sensitiveUpdate: # Normal users cannot change their sensitive data raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="No permission to change sensitive user data" ) elif isAdmin and sensitiveUpdate and not sameMandate: # Admins can only change sensitive data for users of their own mandate raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="No permission to change sensitive data for users of other mandates" ) elif not (isSelfUpdate or (isAdmin and sameMandate) or isSysadmin): # No permission for other cases raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="No permission to update this user" ) # Dynamically filter attributes from the request updateData = {} for attr in userAttributes: if attr in userData and attr != "id": # ID cannot be changed updateData[attr] = userData[attr] # Remove disallowed fields for normal users if not (isAdmin or isSysadmin): updateData = {k: v for k, v in updateData.items() if k in allowedFields} # Update user data updatedUser = context.interfaceData.updateUser(userId, updateData) return updatedUser @router.delete("/{userId}", status_code=status.HTTP_204_NO_CONTENT) async def deleteUser( userId: str = Path(..., description="ID of the user to delete"), currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) ): """Delete a user""" context = await getContext(currentUser) # User exists? userToDelete = context.interfaceData.getUser(userId) if not userToDelete: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {userId} not found" ) # Permission check isSelfDelete = userId == str(context._userId) isAdmin = currentUser.get("privilege") == "admin" isSysadmin = currentUser.get("privilege") == "sysadmin" sameMandate = userToDelete.get("_mandateId") == context._mandateId if isSelfDelete: # User can delete themselves pass elif isAdmin and sameMandate: # Admin can delete users of their own mandate pass elif isSysadmin: # SysAdmin can delete all users pass else: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="No permission to delete this user" ) # Delete user and all referenced objects success = context.interfaceData.deleteUser(userId) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting user with ID {userId}" ) return None