From 29c4bb90e260b97866760c9a948db533188387a5 Mon Sep 17 00:00:00 2001 From: aloftus23 Date: Fri, 6 Dec 2024 12:41:11 -0500 Subject: [PATCH] Fix user and organization endpoints --- .../xfd_api/api_methods/organization.py | 15 +++- .../xfd_django/xfd_api/api_methods/user.py | 83 ++++++++++++------- .../schema_models/organization_schema.py | 1 + .../xfd_django/xfd_api/schema_models/user.py | 1 + backend/src/xfd_django/xfd_api/views.py | 38 +++------ backend/src/xfd_django/xfd_django/asgi.py | 8 +- .../OrganizationList/OrganizationList.tsx | 2 +- frontend/src/pages/Users/UserForm.tsx | 2 +- frontend/src/pages/Users/Users.tsx | 2 +- 9 files changed, 91 insertions(+), 61 deletions(-) diff --git a/backend/src/xfd_django/xfd_api/api_methods/organization.py b/backend/src/xfd_django/xfd_api/api_methods/organization.py index dbdb64dd..0956e229 100644 --- a/backend/src/xfd_django/xfd_api/api_methods/organization.py +++ b/backend/src/xfd_django/xfd_api/api_methods/organization.py @@ -193,6 +193,7 @@ def get_organization(organization_id, current_user): "email": role.user.email, "firstName": role.user.firstName, "lastName": role.user.lastName, + "fullName": role.user.fullName, }, } for role in organization.userRoles.all() @@ -658,6 +659,7 @@ def update_organization(organization_id: str, organization_data, current_user): "email": role.user.email, "firstName": role.user.firstName, "lastName": role.user.lastName, + "fullName": role.user.fullName, }, } for role in organization.userRoles.all() @@ -1031,7 +1033,18 @@ def list_organizations_v2(state, regionId, current_user): "countyFips": org.countyFips, "type": org.type, "userRoles": [ - {"id": str(role.id), "role": role.role, "approved": role.approved} + { + "id": str(role.id), + "role": role.role, + "approved": role.approved, + "user": { + "id": str(role.user.id), + "email": role.user.email, + "firstName": role.user.firstName, + "lastName": role.user.lastName, + "fullName": role.user.fullName, + }, + } for role in org.userRoles.all() ], "tags": [ diff --git a/backend/src/xfd_django/xfd_api/api_methods/user.py b/backend/src/xfd_django/xfd_api/api_methods/user.py index 24d4a804..4978ecde 100644 --- a/backend/src/xfd_django/xfd_api/api_methods/user.py +++ b/backend/src/xfd_django/xfd_api/api_methods/user.py @@ -129,25 +129,24 @@ def accept_terms(version_data, current_user): # DELETE: /users/{userId} -def delete_user(current_user, target_user_id): - """ - Delete a user by ID. - Args: - request : The HTTP request containing authorization and target for deletion.. - - Raises: - HTTPException: If the user is not authorized or the user is not found. +def delete_user(target_user_id, current_user): + """Delete a user by ID.""" + # Validate that the user ID is a valid UUID + if not target_user_id: + raise HTTPException(status_code=404, detail="User not found") - Returns: - JSONResponse: The result of the deletion. - """ + # Check if the current user has permission to access/update this user if not can_access_user(current_user, target_user_id): - return HTTPException(status_code=401, detail="Unauthorized") + raise HTTPException(status_code=403, detail="Unauthorized access.") try: target_user = User.objects.get(id=target_user_id) - result = target_user.delete() - return JSONResponse(status_code=200, content={"result": result}) + target_user.delete() + # Return success response + return { + "status": "success", + "message": f"User {target_user_id} has been deleted successfully.", + } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -155,24 +154,46 @@ def delete_user(current_user, target_user_id): # GET: /users def get_users(current_user): - """ - Retrieve a list of all users. - Args: - current_user : The user making the request. - - Raises: - HTTPException: If the user is not authorized. - - Returns: - List[User]: A list of all users. - """ + """Retrieve a list of all users.""" try: + # Check if user is a regional admin or global admin if not (is_global_view_admin(current_user)): raise HTTPException(status_code=401, detail="Unauthorized") - users = User.objects.all().prefetch_related("roles", "roles.organization") - return [UserSchema.model_validate(user) for user in users] + users = User.objects.all().prefetch_related("roles__organization") + # Return the updated user details + return [ + { + "id": str(user.id), + "createdAt": user.createdAt.isoformat(), + "updatedAt": user.updatedAt.isoformat(), + "firstName": user.firstName, + "lastName": user.lastName, + "fullName": user.fullName, + "email": user.email, + "regionId": user.regionId, + "state": user.state, + "userType": user.userType, + "lastLoggedIn": user.lastLoggedIn, + "acceptedTermsVersion": user.acceptedTermsVersion, + "roles": [ + { + "id": str(role.id), + "approved": role.approved, + "role": role.role, + "organization": { + "id": str(role.organization.id), + "name": role.organization.name, + } + if role.organization + else None, + } + for role in user.roles.all() + ], + } + for user in users + ] except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -280,9 +301,12 @@ def get_users_v2(state, regionId, invitePending, current_user): "state": user.state, "userType": user.userType, "lastLoggedIn": user.lastLoggedIn, + "acceptedTermsVersion": user.acceptedTermsVersion, "roles": [ { "id": str(role.id), + "approved": role.approved, + "role": role.role, "organization": { "id": str(role.organization.id), "name": role.organization.name, @@ -402,9 +426,12 @@ def update_user_v2(user_id, user_data, current_user): "state": updated_user.state, "userType": updated_user.userType, "lastLoggedIn": user.lastLoggedIn, + "acceptedTermsVersion": user.acceptedTermsVersion, "roles": [ { "id": str(role.id), + "approved": role.approved, + "role": role.role, "organization": { "id": str(role.organization.id), "name": role.organization.name, @@ -548,7 +575,7 @@ def invite(new_user_data, current_user): # Always update userType if specified if new_user_data.userType: - user.userType = new_user_data.userType + user.userType = new_user_data.userType.value user.save() # Assign role if an organization is specified diff --git a/backend/src/xfd_django/xfd_api/schema_models/organization_schema.py b/backend/src/xfd_django/xfd_api/schema_models/organization_schema.py index 4fcecdd8..69031769 100644 --- a/backend/src/xfd_django/xfd_api/schema_models/organization_schema.py +++ b/backend/src/xfd_django/xfd_api/schema_models/organization_schema.py @@ -39,6 +39,7 @@ class UserRoleSchema(BaseModel): id: UUID role: str approved: bool + user: dict class TagSchema(BaseModel): diff --git a/backend/src/xfd_django/xfd_api/schema_models/user.py b/backend/src/xfd_django/xfd_api/schema_models/user.py index a286a780..cd0fba70 100644 --- a/backend/src/xfd_django/xfd_api/schema_models/user.py +++ b/backend/src/xfd_django/xfd_api/schema_models/user.py @@ -181,6 +181,7 @@ class UserResponseV2(BaseModel): lastName: str fullName: str email: str + acceptedTermsVersion: Optional[str] = None lastLoggedIn: Optional[datetime] = None regionId: Optional[str] = None state: Optional[str] = None diff --git a/backend/src/xfd_django/xfd_api/views.py b/backend/src/xfd_django/xfd_api/views.py index 410b82b0..fbf8c6e4 100644 --- a/backend/src/xfd_django/xfd_api/views.py +++ b/backend/src/xfd_django/xfd_api/views.py @@ -384,44 +384,32 @@ async def call_accept_terms( return accept_terms(version_data, current_user) -# GET Current User @api_router.get("/users/me", tags=["Users"]) async def read_users_me(current_user: User = Depends(get_current_active_user)): return get_me(current_user) -@api_router.delete("/users/{userId}", tags=["Users"]) -async def call_delete_user(current_user, userId: str): - """ - call delete_user() - Args: - userId: UUID of the user to delete. - Returns: - User: The user that was deleted. - """ - - return delete_user(current_user, userId) +@api_router.delete( + "/users/{userId}", + response_model=OrganizationSchema.GenericMessageResponseModel, + dependencies=[Depends(get_current_active_user)], + tags=["Users"], +) +async def call_delete_user( + userId: str, current_user: User = Depends(get_current_active_user) +): + """Delete user.""" + return delete_user(userId, current_user) @api_router.get( "/users", - response_model=List[UserSchema], + response_model=List[UserResponseV2], dependencies=[Depends(get_current_active_user)], tags=["Users"], ) async def call_get_users(current_user: User = Depends(get_current_active_user)): - """ - Call get_users() - - Args: - regionId: Region IDs to filter users by. - - Raises: - HTTPException: If the user is not authorized or no users are found. - - Returns: - List[User]: A list of users matching the filter criteria. - """ + """Get all users.""" return get_users(current_user) diff --git a/backend/src/xfd_django/xfd_django/asgi.py b/backend/src/xfd_django/xfd_django/asgi.py index 83dd6aa6..e1346de6 100644 --- a/backend/src/xfd_django/xfd_django/asgi.py +++ b/backend/src/xfd_django/xfd_django/asgi.py @@ -34,7 +34,7 @@ "'self'", os.getenv("COGNITO_URL"), os.getenv("BACKEND_DOMAIN"), - "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js" + "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js", ], "frame-src": ["'self'", "https://www.dhs.gov/ntas/"], "img-src": [ @@ -43,7 +43,7 @@ os.getenv("FRONTEND_DOMAIN"), "https://www.ssa.gov", "https://www.dhs.gov", - "https://fastapi.tiangolo.com/img/favicon.png" + "https://fastapi.tiangolo.com/img/favicon.png", ], "object-src": ["'none'"], "script-src": [ @@ -59,8 +59,8 @@ "style-src": [ "'self'", "'unsafe-inline'", - "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css" - ], + "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css", + ], "frame-ancestors": ["'none'"], } diff --git a/frontend/src/components/OrganizationList/OrganizationList.tsx b/frontend/src/components/OrganizationList/OrganizationList.tsx index 87cda1c5..39137c94 100644 --- a/frontend/src/components/OrganizationList/OrganizationList.tsx +++ b/frontend/src/components/OrganizationList/OrganizationList.tsx @@ -66,7 +66,7 @@ export const OrganizationList: React.FC<{ const onSubmit = async (body: Object) => { try { - const org = await apiPost('/organizations/', { + const org = await apiPost('/organizations', { body }); setOrganizations(organizations.concat(org)); diff --git a/frontend/src/pages/Users/UserForm.tsx b/frontend/src/pages/Users/UserForm.tsx index 1f6ae72e..00e0eba9 100644 --- a/frontend/src/pages/Users/UserForm.tsx +++ b/frontend/src/pages/Users/UserForm.tsx @@ -181,7 +181,7 @@ export const UserForm: React.FC = ({ regionId: values.regionId }; try { - const user = await apiPost('/users/', { + const user = await apiPost('/users', { body }); user.fullName = `${user.firstName} ${user.lastName}`; diff --git a/frontend/src/pages/Users/Users.tsx b/frontend/src/pages/Users/Users.tsx index 1d9b1634..9c696ec1 100644 --- a/frontend/src/pages/Users/Users.tsx +++ b/frontend/src/pages/Users/Users.tsx @@ -75,7 +75,7 @@ export const Users: React.FC = () => { const fetchUsers = useCallback(async () => { setIsLoading(true); try { - const rows = await apiGet(`/users/`); + const rows = await apiGet(`/users`); rows.forEach((row) => { row.lastLoggedInString = row.lastLoggedIn ? format(parseISO(row.lastLoggedIn), 'MM-dd-yyyy hh:mm a')