feat: add linking UGs to Users

This commit is contained in:
swve 2024-03-30 17:32:29 +00:00
parent d1d817678b
commit e173a32e3c
9 changed files with 235 additions and 20 deletions

View file

@ -1,22 +1,20 @@
from typing import Literal from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
from sqlmodel import Session 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.usergroups import UserGroupCreate, UserGroupRead, UserGroupUpdate
from src.db.users import PublicUser from src.db.users import PublicUser, UserRead
from src.services.users.usergroups import ( from src.services.users.usergroups import (
add_resources_to_usergroup, add_resources_to_usergroup,
add_users_to_usergroup, add_users_to_usergroup,
create_usergroup, create_usergroup,
delete_usergroup_by_id, delete_usergroup_by_id,
get_usergroups_by_resource, get_usergroups_by_resource,
get_users_linked_to_usergroup,
read_usergroup_by_id, read_usergroup_by_id,
read_usergroups_by_org_id, read_usergroups_by_org_id,
remove_resources_from_usergroup, remove_resources_from_usergroup,
remove_users_from_usergroup, remove_users_from_usergroup,
update_usergroup_by_id, update_usergroup_by_id,
) )
from src.services.orgs.orgs import get_org_join_mechanism
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.core.events.database import get_db_session 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) 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"]) @router.get("/org/{org_id}", response_model=list[UserGroupRead], tags=["usergroups"])
async def api_get_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) 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( async def api_get_usergroupsby_resource(
*, *,
request: Request, request: Request,
@ -76,7 +93,9 @@ async def api_get_usergroupsby_resource(
""" """
Get UserGroups by Org 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"]) @router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"])

View file

@ -1,10 +1,7 @@
import stat
from typing import Literal from typing import Literal
from uuid import uuid4 from uuid import uuid4
from regex import R from sqlalchemy import union
from sqlalchemy import exists, union from sqlmodel import Session, select
from sqlmodel import Session, select, and_, or_
from src.db.usergroups import UserGroup
from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_resources import UserGroupResource
from src.db.usergroup_user import UserGroupUser from src.db.usergroup_user import UserGroupUser
from src.db.organizations import Organization from src.db.organizations import Organization

View file

@ -6,14 +6,13 @@ from fastapi import HTTPException, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_based_on_roles_and_authorship_and_usergroups,
authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_resources import UserGroupResource
from src.db.usergroup_user import UserGroupUser from src.db.usergroup_user import UserGroupUser
from src.db.organizations import Organization from src.db.organizations import Organization
from src.db.usergroups import UserGroup, UserGroupCreate, UserGroupRead, UserGroupUpdate 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( async def create_usergroup(
@ -89,6 +88,48 @@ async def read_usergroup_by_id(
return usergroup 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( async def read_usergroups_by_org_id(
request: Request, request: Request,
db_session: Session, db_session: Session,
@ -99,8 +140,6 @@ async def read_usergroups_by_org_id(
statement = select(UserGroup).where(UserGroup.org_id == org_id) statement = select(UserGroup).where(UserGroup.org_id == org_id)
usergroups = db_session.exec(statement).all() usergroups = db_session.exec(statement).all()
# RBAC check # RBAC check
await rbac_check( await rbac_check(
request, request,
@ -237,6 +276,8 @@ async def add_users_to_usergroup(
detail="UserGroup not found", detail="UserGroup not found",
) )
# RBAC check # RBAC check
await rbac_check( await rbac_check(
request, request,
@ -252,6 +293,17 @@ async def add_users_to_usergroup(
statement = select(User).where(User.id == user_id) statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first() 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: if user:
# Add user to UserGroup # Add user to UserGroup
if user.id is not None: if user.id is not None:
@ -301,7 +353,7 @@ async def remove_users_from_usergroup(
user_ids_array = user_ids.split(",") user_ids_array = user_ids.split(",")
for user_id in user_ids_array: 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() usergroup_user = db_session.exec(statement).first()
if usergroup_user: if usergroup_user:

View file

@ -66,6 +66,7 @@ function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) {
last_name: '', last_name: '',
}, },
validate, validate,
enableReinitialize: true,
onSubmit: async (values) => { onSubmit: async (values) => {
setError('') setError('')
setMessage('') setMessage('')

View file

@ -62,6 +62,7 @@ function OpenSignUpComponent() {
last_name: '', last_name: '',
}, },
validate, validate,
enableReinitialize: true,
onSubmit: async (values) => { onSubmit: async (values) => {
setError('') setError('')
setMessage('') setMessage('')

View file

@ -74,9 +74,11 @@ function OrgUserGroups() {
onOpenChange={() => onOpenChange={() =>
handleUserGroupManagementModal(usergroup.id) handleUserGroupManagementModal(usergroup.id)
} }
minHeight="no-min" minHeight="lg"
minWidth='lg'
dialogContent={ dialogContent={
<ManageUsers <ManageUsers
usergroup_id={usergroup.id}
/> />
} }
dialogTitle="Manage UserGroup Users" dialogTitle="Manage UserGroup Users"

View file

@ -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 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 ( return (
<div>ManageUsers</div> <div className='py-3'>
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
<tr className="font-bolder text-sm">
<th className="py-3 px-4">User</th>
<th className="py-3 px-4">Linked</th>
<th className="py-3 px-4">Actions</th>
</tr>
</thead>
<>
<tbody className="mt-5 bg-white rounded-md">
{OrgUsers?.map((user: any) => (
<tr
key={user.user.id}
className="border-b border-gray-200 border-dashed text-sm"
>
<td className="py-3 px-4 flex space-x-2 items-center">
<span>
{user.user.first_name + ' ' + user.user.last_name}
</span>
<span className="text-xs bg-neutral-100 p-1 px-2 rounded-full text-neutral-400 font-semibold">
@{user.user.username}
</span>
</td>
<td className="py-3 px-4">
{isUserPartOfGroup(user.user.id) ?
<div className="space-x-1 flex w-fit px-4 py-1 bg-cyan-100 rounded-full items-center text-cyan-800">
<Check size={16} />
<span>Linked</span>
</div>
:
<div className="space-x-1 flex w-fit px-4 py-1 bg-gray-100 rounded-full items-center text-gray-800">
<X size={16} />
<span>Not linked</span>
</div>
}
</td>
<td className="py-3 px-4 flex space-x-2 items-end">
<button
onClick={() => handleLinkUser(user.user.id)}
className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-cyan-700 rounded-md font-bold items-center text-sm text-cyan-100">
<Plus className="w-4 h-4" />
<span> Link</span>
</button>
<button
onClick={() => handleUnlinkUser(user.user.id)}
className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-gray-700 rounded-md font-bold items-center text-sm text-gray-100">
<X className="w-4 h-4" />
<span> Unlink</span>
</button>
</td>
</tr>
))}
</tbody>
</>
</table>
</div>
) )
} }

View file

@ -15,6 +15,7 @@ type ModalParams = {
onOpenChange: any onOpenChange: any
isDialogOpen?: boolean isDialogOpen?: boolean
minHeight?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min' minHeight?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
minWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
} }
const Modal = (params: ModalParams) => ( const Modal = (params: ModalParams) => (
@ -28,6 +29,7 @@ const Modal = (params: ModalParams) => (
<DialogContent <DialogContent
className="overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full" className="overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full"
minHeight={params.minHeight} minHeight={params.minHeight}
minWidth={params.minWidth}
> >
<DialogTopBar className="-space-y-1"> <DialogTopBar className="-space-y-1">
<DialogTitle>{params.dialogTitle}</DialogTitle> <DialogTitle>{params.dialogTitle}</DialogTitle>
@ -103,6 +105,23 @@ const DialogContent = styled(Dialog.Content, {
minHeight: '900px', minHeight: '900px',
}, },
}, },
minWidth: {
'no-min': {
minWidth: '0px',
},
sm: {
minWidth: '600px',
},
md: {
minWidth: '800px',
},
lg: {
minWidth: '1000px',
},
xl: {
minWidth: '1200px',
},
},
}, },
backgroundColor: 'white', backgroundColor: 'white',

View file

@ -19,6 +19,24 @@ export async function createUserGroup(body: any) {
return res 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) { export async function deleteUserGroup(usergroup_id: number) {
const result: any = await fetch( const result: any = await fetch(
`${getAPIUrl()}usergroups/${usergroup_id}`, `${getAPIUrl()}usergroups/${usergroup_id}`,