From e173a32e3cf61837e50880fbc9f49133baeb10d0 Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 30 Mar 2024 17:32:29 +0000 Subject: [PATCH] feat: add linking UGs to Users --- apps/api/src/routers/usergroups.py | 33 ++++-- apps/api/src/services/courses/courses.py | 7 +- apps/api/src/services/users/usergroups.py | 62 +++++++++- .../[orgslug]/signup/InviteOnlySignUp.tsx | 1 + .../app/orgs/[orgslug]/signup/OpenSignup.tsx | 1 + .../Users/OrgUserGroups/OrgUserGroups.tsx | 4 +- .../Modals/Dash/OrgUserGroups/ManageUsers.tsx | 110 +++++++++++++++++- .../components/StyledElements/Modal/Modal.tsx | 19 +++ apps/web/services/usergroups/usergroups.ts | 18 +++ 9 files changed, 235 insertions(+), 20 deletions(-) diff --git a/apps/api/src/routers/usergroups.py b/apps/api/src/routers/usergroups.py index 74160f43..5c2c30b4 100644 --- a/apps/api/src/routers/usergroups.py +++ b/apps/api/src/routers/usergroups.py @@ -1,22 +1,20 @@ -from typing import Literal -from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile +from fastapi import APIRouter, Depends, Request from sqlmodel import Session -from src.services.users.users import delete_user_by_id from src.db.usergroups import UserGroupCreate, UserGroupRead, UserGroupUpdate -from src.db.users import PublicUser +from src.db.users import PublicUser, UserRead from src.services.users.usergroups import ( add_resources_to_usergroup, add_users_to_usergroup, create_usergroup, delete_usergroup_by_id, get_usergroups_by_resource, + get_users_linked_to_usergroup, read_usergroup_by_id, read_usergroups_by_org_id, remove_resources_from_usergroup, remove_users_from_usergroup, update_usergroup_by_id, ) -from src.services.orgs.orgs import get_org_join_mechanism from src.security.auth import get_current_user from src.core.events.database import get_db_session @@ -52,6 +50,22 @@ async def api_get_usergroup( return await read_usergroup_by_id(request, db_session, current_user, usergroup_id) +@router.get("/{usergroup_id}/users", response_model=list[UserRead], tags=["usergroups"]) +async def api_get_users_linked_to_usergroup( + *, + request: Request, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), + usergroup_id: int, +) -> list[UserRead]: + """ + Get Users linked to UserGroup + """ + return await get_users_linked_to_usergroup( + request, db_session, current_user, usergroup_id + ) + + @router.get("/org/{org_id}", response_model=list[UserGroupRead], tags=["usergroups"]) async def api_get_usergroups( *, @@ -65,7 +79,10 @@ async def api_get_usergroups( """ return await read_usergroups_by_org_id(request, db_session, current_user, org_id) -@router.get("/resource/{resource_uuid}", response_model=list[UserGroupRead], tags=["usergroups"]) + +@router.get( + "/resource/{resource_uuid}", response_model=list[UserGroupRead], tags=["usergroups"] +) async def api_get_usergroupsby_resource( *, request: Request, @@ -76,7 +93,9 @@ async def api_get_usergroupsby_resource( """ Get UserGroups by Org """ - return await get_usergroups_by_resource(request, db_session, current_user, resource_uuid) + return await get_usergroups_by_resource( + request, db_session, current_user, resource_uuid + ) @router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"]) diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index e240bee4..3e2458ea 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -1,10 +1,7 @@ -import stat from typing import Literal from uuid import uuid4 -from regex import R -from sqlalchemy import exists, union -from sqlmodel import Session, select, and_, or_ -from src.db.usergroups import UserGroup +from sqlalchemy import union +from sqlmodel import Session, select from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_user import UserGroupUser from src.db.organizations import Organization diff --git a/apps/api/src/services/users/usergroups.py b/apps/api/src/services/users/usergroups.py index cc497762..1ad62e4b 100644 --- a/apps/api/src/services/users/usergroups.py +++ b/apps/api/src/services/users/usergroups.py @@ -6,14 +6,13 @@ from fastapi import HTTPException, Request from sqlmodel import Session, select from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship_and_usergroups, - authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_user import UserGroupUser from src.db.organizations import Organization from src.db.usergroups import UserGroup, UserGroupCreate, UserGroupRead, UserGroupUpdate -from src.db.users import AnonymousUser, PublicUser, User +from src.db.users import AnonymousUser, PublicUser, User, UserRead async def create_usergroup( @@ -89,6 +88,48 @@ async def read_usergroup_by_id( return usergroup +async def get_users_linked_to_usergroup( + request: Request, + db_session: Session, + current_user: PublicUser | AnonymousUser, + usergroup_id: int, +) -> list[UserRead]: + + statement = select(UserGroup).where(UserGroup.id == usergroup_id) + usergroup = db_session.exec(statement).first() + + if not usergroup: + raise HTTPException( + status_code=404, + detail="UserGroup not found", + ) + + # RBAC check + await rbac_check( + request, + usergroup_uuid=usergroup.usergroup_uuid, + current_user=current_user, + action="read", + db_session=db_session, + ) + + statement = select(UserGroupUser).where(UserGroupUser.usergroup_id == usergroup_id) + usergroup_users = db_session.exec(statement).all() + + user_ids = [usergroup_user.user_id for usergroup_user in usergroup_users] + + # get users + users = [] + for user_id in user_ids: + statement = select(User).where(User.id == user_id) + user = db_session.exec(statement).first() + users.append(user) + + users = [UserRead.from_orm(user) for user in users] + + return users + + async def read_usergroups_by_org_id( request: Request, db_session: Session, @@ -99,8 +140,6 @@ async def read_usergroups_by_org_id( statement = select(UserGroup).where(UserGroup.org_id == org_id) usergroups = db_session.exec(statement).all() - - # RBAC check await rbac_check( request, @@ -236,6 +275,8 @@ async def add_users_to_usergroup( status_code=404, detail="UserGroup not found", ) + + # RBAC check await rbac_check( @@ -252,6 +293,17 @@ async def add_users_to_usergroup( statement = select(User).where(User.id == user_id) user = db_session.exec(statement).first() + # Check if User is already Linked to UserGroup + statement = select(UserGroupUser).where( + UserGroupUser.usergroup_id == usergroup_id, + UserGroupUser.user_id == user_id, + ) + usergroup_user = db_session.exec(statement).first() + + if usergroup_user: + logging.error(f"User with id {user_id} already exists in UserGroup") + continue + if user: # Add user to UserGroup if user.id is not None: @@ -301,7 +353,7 @@ async def remove_users_from_usergroup( user_ids_array = user_ids.split(",") for user_id in user_ids_array: - statement = select(UserGroupUser).where(UserGroupUser.user_id == user_id) + statement = select(UserGroupUser).where(UserGroupUser.user_id == user_id, UserGroupUser.usergroup_id == usergroup_id) usergroup_user = db_session.exec(statement).first() if usergroup_user: diff --git a/apps/web/app/orgs/[orgslug]/signup/InviteOnlySignUp.tsx b/apps/web/app/orgs/[orgslug]/signup/InviteOnlySignUp.tsx index 0e9f93c8..6827fc20 100644 --- a/apps/web/app/orgs/[orgslug]/signup/InviteOnlySignUp.tsx +++ b/apps/web/app/orgs/[orgslug]/signup/InviteOnlySignUp.tsx @@ -66,6 +66,7 @@ function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) { last_name: '', }, validate, + enableReinitialize: true, onSubmit: async (values) => { setError('') setMessage('') diff --git a/apps/web/app/orgs/[orgslug]/signup/OpenSignup.tsx b/apps/web/app/orgs/[orgslug]/signup/OpenSignup.tsx index a4b6333a..9c4bbc4d 100644 --- a/apps/web/app/orgs/[orgslug]/signup/OpenSignup.tsx +++ b/apps/web/app/orgs/[orgslug]/signup/OpenSignup.tsx @@ -62,6 +62,7 @@ function OpenSignUpComponent() { last_name: '', }, validate, + enableReinitialize: true, onSubmit: async (values) => { setError('') setMessage('') diff --git a/apps/web/components/Dashboard/Users/OrgUserGroups/OrgUserGroups.tsx b/apps/web/components/Dashboard/Users/OrgUserGroups/OrgUserGroups.tsx index fa84eca3..8a874b95 100644 --- a/apps/web/components/Dashboard/Users/OrgUserGroups/OrgUserGroups.tsx +++ b/apps/web/components/Dashboard/Users/OrgUserGroups/OrgUserGroups.tsx @@ -74,9 +74,11 @@ function OrgUserGroups() { onOpenChange={() => handleUserGroupManagementModal(usergroup.id) } - minHeight="no-min" + minHeight="lg" + minWidth='lg' dialogContent={ } dialogTitle="Manage UserGroup Users" diff --git a/apps/web/components/Objects/Modals/Dash/OrgUserGroups/ManageUsers.tsx b/apps/web/components/Objects/Modals/Dash/OrgUserGroups/ManageUsers.tsx index 150ce4b6..ce7d35e2 100644 --- a/apps/web/components/Objects/Modals/Dash/OrgUserGroups/ManageUsers.tsx +++ b/apps/web/components/Objects/Modals/Dash/OrgUserGroups/ManageUsers.tsx @@ -1,8 +1,114 @@ +import { useOrg } from '@components/Contexts/OrgContext' +import { getAPIUrl } from '@services/config/config' +import { linkUserToUserGroup, unLinkUserToUserGroup } from '@services/usergroups/usergroups' +import { swrFetcher } from '@services/utils/ts/requests' +import { Check, Plus, X } from 'lucide-react' import React from 'react' +import toast from 'react-hot-toast' +import useSWR, { mutate } from 'swr' + + +type ManageUsersProps = { + usergroup_id: any +} + +function ManageUsers(props: ManageUsersProps) { + const org = useOrg() as any + const { data: OrgUsers } = useSWR( + org ? `${getAPIUrl()}orgs/${org.id}/users` : null, + swrFetcher + ) + const { data: UGusers } = useSWR( + org ? `${getAPIUrl()}usergroups/${props.usergroup_id}/users` : null, + swrFetcher + ) + + const isUserPartOfGroup = (user_id: any) => { + if (UGusers) { + return UGusers.some((user: any) => user.id === user_id) + } + return false + } + + const handleLinkUser = async (user_id: any) => { + const res = await linkUserToUserGroup(props.usergroup_id, user_id) + if (res.status === 200) { + toast.success('User linked successfully') + mutate(`${getAPIUrl()}usergroups/${props.usergroup_id}/users`) + } else { + toast.error('Error ' + res.status + ': ' + res.data.detail) + } + } + + const handleUnlinkUser = async (user_id: any) => { + const res = await unLinkUserToUserGroup(props.usergroup_id, user_id) + if (res.status === 200) { + toast.success('User unlinked successfully') + mutate(`${getAPIUrl()}usergroups/${props.usergroup_id}/users`) + } else { + toast.error('Error ' + res.status + ': ' + res.data.detail) + } + } -function ManageUsers() { return ( -
ManageUsers
+
+ + + + + + + + + <> + + {OrgUsers?.map((user: any) => ( + + + + + + ))} + + +
UserLinkedActions
+ + {user.user.first_name + ' ' + user.user.last_name} + + + @{user.user.username} + + + {isUserPartOfGroup(user.user.id) ? +
+ + Linked +
+ : +
+ + Not linked +
+ } +
+ + + +
+
) } diff --git a/apps/web/components/StyledElements/Modal/Modal.tsx b/apps/web/components/StyledElements/Modal/Modal.tsx index fcbda31f..51e12678 100644 --- a/apps/web/components/StyledElements/Modal/Modal.tsx +++ b/apps/web/components/StyledElements/Modal/Modal.tsx @@ -15,6 +15,7 @@ type ModalParams = { onOpenChange: any isDialogOpen?: boolean minHeight?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min' + minWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min' } const Modal = (params: ModalParams) => ( @@ -28,6 +29,7 @@ const Modal = (params: ModalParams) => ( {params.dialogTitle} @@ -103,6 +105,23 @@ const DialogContent = styled(Dialog.Content, { minHeight: '900px', }, }, + minWidth: { + 'no-min': { + minWidth: '0px', + }, + sm: { + minWidth: '600px', + }, + md: { + minWidth: '800px', + }, + lg: { + minWidth: '1000px', + }, + xl: { + minWidth: '1200px', + }, + }, }, backgroundColor: 'white', diff --git a/apps/web/services/usergroups/usergroups.ts b/apps/web/services/usergroups/usergroups.ts index 26f4c0f7..9bd42ce0 100644 --- a/apps/web/services/usergroups/usergroups.ts +++ b/apps/web/services/usergroups/usergroups.ts @@ -19,6 +19,24 @@ export async function createUserGroup(body: any) { return res } +export async function linkUserToUserGroup(usergroup_id: any, user_id: any) { + const result: any = await fetch( + `${getAPIUrl()}usergroups/${usergroup_id}/add_users?user_ids=${user_id}`, + RequestBody('POST', null, null) + ) + const res = await getResponseMetadata(result) + return res +} + +export async function unLinkUserToUserGroup(usergroup_id: any, user_id: any) { + const result: any = await fetch( + `${getAPIUrl()}usergroups/${usergroup_id}/remove_users?user_ids=${user_id}`, + RequestBody('DELETE', null, null) + ) + const res = await getResponseMetadata(result) + return res +} + export async function deleteUserGroup(usergroup_id: number) { const result: any = await fetch( `${getAPIUrl()}usergroups/${usergroup_id}`,