mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: users management
This commit is contained in:
parent
a552300e15
commit
689625b0d5
22 changed files with 621 additions and 36 deletions
|
|
@ -1,5 +1,8 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
from src.db.roles import RoleRead
|
||||||
|
|
||||||
from src.db.organization_config import OrganizationConfig
|
from src.db.organization_config import OrganizationConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,3 +35,9 @@ class OrganizationRead(OrganizationBase):
|
||||||
config: Optional[OrganizationConfig | dict]
|
config: Optional[OrganizationConfig | dict]
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationUser(BaseModel):
|
||||||
|
from src.db.users import UserRead
|
||||||
|
user: UserRead
|
||||||
|
role: RoleRead
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
from src.db.roles import RoleRead
|
from src.db.roles import RoleRead
|
||||||
from src.db.organizations import OrganizationRead
|
|
||||||
|
|
||||||
|
|
||||||
class UserBase(SQLModel):
|
class UserBase(SQLModel):
|
||||||
|
|
@ -45,6 +44,7 @@ class PublicUser(UserRead):
|
||||||
|
|
||||||
|
|
||||||
class UserRoleWithOrg(BaseModel):
|
class UserRoleWithOrg(BaseModel):
|
||||||
|
from src.db.organizations import OrganizationRead
|
||||||
role: RoleRead
|
role: RoleRead
|
||||||
org: OrganizationRead
|
org: OrganizationRead
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from src.db.organizations import (
|
||||||
OrganizationCreate,
|
OrganizationCreate,
|
||||||
OrganizationRead,
|
OrganizationRead,
|
||||||
OrganizationUpdate,
|
OrganizationUpdate,
|
||||||
|
OrganizationUser,
|
||||||
)
|
)
|
||||||
from src.core.events.database import get_db_session
|
from src.core.events.database import get_db_session
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
|
|
@ -17,9 +18,12 @@ from src.services.orgs.orgs import (
|
||||||
delete_org,
|
delete_org,
|
||||||
get_organization,
|
get_organization,
|
||||||
get_organization_by_slug,
|
get_organization_by_slug,
|
||||||
|
get_organization_users,
|
||||||
get_orgs_by_user,
|
get_orgs_by_user,
|
||||||
|
remove_user_from_org,
|
||||||
update_org,
|
update_org,
|
||||||
update_org_logo,
|
update_org_logo,
|
||||||
|
update_user_role,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -69,6 +73,52 @@ async def api_get_org(
|
||||||
return await get_organization(request, org_id, db_session, current_user)
|
return await get_organization(request, org_id, db_session, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{org_id}/users")
|
||||||
|
async def api_get_org_users(
|
||||||
|
request: Request,
|
||||||
|
org_id: str,
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
) -> list[OrganizationUser]:
|
||||||
|
"""
|
||||||
|
Get single Org by ID
|
||||||
|
"""
|
||||||
|
return await get_organization_users(request, org_id, db_session, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
|
||||||
|
async def api_update_user_role(
|
||||||
|
request: Request,
|
||||||
|
org_id: str,
|
||||||
|
user_id: str,
|
||||||
|
role_uuid: str,
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update user role
|
||||||
|
"""
|
||||||
|
return await update_user_role(
|
||||||
|
request, org_id, user_id, role_uuid, db_session, current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{org_id}/users/{user_id}")
|
||||||
|
async def api_remove_user_from_org(
|
||||||
|
request: Request,
|
||||||
|
org_id: int,
|
||||||
|
user_id: int,
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Remove user from org
|
||||||
|
"""
|
||||||
|
return await remove_user_from_org(
|
||||||
|
request, org_id, user_id, db_session, current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/slug/{org_slug}")
|
@router.get("/slug/{org_slug}")
|
||||||
async def api_get_org_by_slug(
|
async def api_get_org_by_slug(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,34 @@ async def authorization_verify_based_on_roles(
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def authorization_verify_based_on_org_admin_status(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
action: Literal["read", "update", "delete", "create"],
|
||||||
|
element_uuid: str,
|
||||||
|
db_session: Session,
|
||||||
|
):
|
||||||
|
await check_element_type(element_uuid)
|
||||||
|
|
||||||
|
# Get user roles bound to an organization and standard roles
|
||||||
|
statement = (
|
||||||
|
select(Role)
|
||||||
|
.join(UserOrganization)
|
||||||
|
.where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null()))
|
||||||
|
.where(UserOrganization.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
# Find in roles list if there is a role that matches users action for this type of element
|
||||||
|
for role in user_roles_in_organization_and_standard_roles:
|
||||||
|
role = Role.from_orm(role)
|
||||||
|
if role.id == 1 or role.id == 2:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Tested and working
|
# Tested and working
|
||||||
async def authorization_verify_based_on_roles_and_authorship(
|
async def authorization_verify_based_on_roles_and_authorship(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ async def get_course_chapters(
|
||||||
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
|
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
|
||||||
|
|
||||||
# RBAC check
|
# RBAC check
|
||||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
await rbac_check(request, course.course_uuid, current_user, "read", db_session) # type: ignore
|
||||||
|
|
||||||
# Get activities for each chapter
|
# Get activities for each chapter
|
||||||
for chapter in chapters:
|
for chapter in chapters:
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ async def update_course_thumbnail(
|
||||||
if thumbnail_file and thumbnail_file.filename:
|
if thumbnail_file and thumbnail_file.filename:
|
||||||
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||||
await upload_thumbnail(
|
await upload_thumbnail(
|
||||||
thumbnail_file, name_in_disk, 'users', course.course_uuid
|
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update course
|
# Update course
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
from src.db.roles import Role, RoleRead
|
||||||
from src.db.organization_config import (
|
from src.db.organization_config import (
|
||||||
AIConfig,
|
AIConfig,
|
||||||
AIEnabledFeatures,
|
AIEnabledFeatures,
|
||||||
|
|
@ -15,16 +16,18 @@ from src.db.organization_config import (
|
||||||
OrganizationConfigBase,
|
OrganizationConfigBase,
|
||||||
)
|
)
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_org_admin_status,
|
||||||
|
authorization_verify_based_on_roles,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.db.users import AnonymousUser, PublicUser
|
from src.db.users import AnonymousUser, PublicUser, User, UserRead
|
||||||
from src.db.user_organizations import UserOrganization
|
from src.db.user_organizations import UserOrganization
|
||||||
from src.db.organizations import (
|
from src.db.organizations import (
|
||||||
Organization,
|
Organization,
|
||||||
OrganizationCreate,
|
OrganizationCreate,
|
||||||
OrganizationRead,
|
OrganizationRead,
|
||||||
OrganizationUpdate,
|
OrganizationUpdate,
|
||||||
|
OrganizationUser,
|
||||||
)
|
)
|
||||||
from src.services.orgs.logos import upload_org_logo
|
from src.services.orgs.logos import upload_org_logo
|
||||||
from fastapi import HTTPException, UploadFile, status, Request
|
from fastapi import HTTPException, UploadFile, status, Request
|
||||||
|
|
@ -66,6 +69,199 @@ async def get_organization(
|
||||||
return org
|
return org
|
||||||
|
|
||||||
|
|
||||||
|
async def get_organization_users(
|
||||||
|
request: Request,
|
||||||
|
org_id: str,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
) -> list[OrganizationUser]:
|
||||||
|
statement = select(Organization).where(Organization.id == org_id)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
org = result.first()
|
||||||
|
|
||||||
|
if not org:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Organization not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||||
|
|
||||||
|
statement = (
|
||||||
|
select(User)
|
||||||
|
.join(UserOrganization)
|
||||||
|
.join(Organization)
|
||||||
|
.where(Organization.id == org_id)
|
||||||
|
)
|
||||||
|
users = db_session.exec(statement)
|
||||||
|
users = users.all()
|
||||||
|
|
||||||
|
org_users_list = []
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
statement = select(UserOrganization).where(
|
||||||
|
UserOrganization.user_id == user.id, UserOrganization.org_id == org_id
|
||||||
|
)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
user_org = result.first()
|
||||||
|
|
||||||
|
if not user_org:
|
||||||
|
logging.error(f"User {user.id} not found")
|
||||||
|
|
||||||
|
# skip this user
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
statement = select(Role).where(Role.id == user_org.role_id)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
role = result.first()
|
||||||
|
|
||||||
|
if not role:
|
||||||
|
logging.error(f"Role {user_org.role_id} not found")
|
||||||
|
|
||||||
|
# skip this user
|
||||||
|
continue
|
||||||
|
|
||||||
|
statement = select(User).where(User.id == user_org.user_id)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
user = result.first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
logging.error(f"User {user_org.user_id} not found")
|
||||||
|
|
||||||
|
# skip this user
|
||||||
|
continue
|
||||||
|
|
||||||
|
user = UserRead.from_orm(user)
|
||||||
|
role = RoleRead.from_orm(role)
|
||||||
|
|
||||||
|
org_user = OrganizationUser(
|
||||||
|
user=user,
|
||||||
|
role=role,
|
||||||
|
)
|
||||||
|
|
||||||
|
org_users_list.append(org_user)
|
||||||
|
|
||||||
|
return org_users_list
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_user_from_org(
|
||||||
|
request: Request,
|
||||||
|
org_id: int,
|
||||||
|
user_id: int,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
):
|
||||||
|
statement = select(Organization).where(Organization.id == org_id)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
org = result.first()
|
||||||
|
|
||||||
|
if not org:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Organization not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||||
|
|
||||||
|
statement = select(UserOrganization).where(
|
||||||
|
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
|
||||||
|
)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
user_org = result.first()
|
||||||
|
|
||||||
|
if not user_org:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="User not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user is the last admin
|
||||||
|
statement = select(UserOrganization).where(
|
||||||
|
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
|
||||||
|
)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
admins = result.all()
|
||||||
|
|
||||||
|
if len(admins) == 1 and admins[0].user_id == user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="You can't remove the last admin of the organization",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
db_session.delete(user_org)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
return {"detail": "User removed from org"}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_user_role(
|
||||||
|
request: Request,
|
||||||
|
org_id: str,
|
||||||
|
user_id: str,
|
||||||
|
role_uuid: str,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
):
|
||||||
|
# find role
|
||||||
|
statement = select(Role).where(Role.role_uuid == role_uuid)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
role = result.first()
|
||||||
|
|
||||||
|
if not role:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Role not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
role_id = role.id
|
||||||
|
|
||||||
|
statement = select(Organization).where(Organization.id == org_id)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
org = result.first()
|
||||||
|
|
||||||
|
if not org:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Organization not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||||
|
|
||||||
|
statement = select(UserOrganization).where(
|
||||||
|
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
|
||||||
|
)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
user_org = result.first()
|
||||||
|
|
||||||
|
if not user_org:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="User not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
if role_id is not None:
|
||||||
|
user_org.role_id = role_id
|
||||||
|
|
||||||
|
db_session.add(user_org)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(user_org)
|
||||||
|
|
||||||
|
return {"detail": "User role updated"}
|
||||||
|
|
||||||
|
|
||||||
async def get_organization_by_slug(
|
async def get_organization_by_slug(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_slug: str,
|
org_slug: str,
|
||||||
|
|
@ -443,7 +639,7 @@ async def get_orgs_by_user(
|
||||||
|
|
||||||
async def rbac_check(
|
async def rbac_check(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: str,
|
org_uuid: str,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
action: Literal["create", "read", "update", "delete"],
|
action: Literal["create", "read", "update", "delete"],
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
@ -455,8 +651,12 @@ async def rbac_check(
|
||||||
else:
|
else:
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles(
|
||||||
request, current_user.id, action, org_id, db_session
|
request, current_user.id, action, org_uuid, db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
await authorization_verify_based_on_org_admin_status(
|
||||||
|
request, current_user.id, action, org_uuid, db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ async def upload_avatar(avatar_file, name_in_disk, user_uuid):
|
||||||
contents = avatar_file.file.read()
|
contents = avatar_file.file.read()
|
||||||
try:
|
try:
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"avatars",
|
"avatars",
|
||||||
"users",
|
"users",
|
||||||
user_uuid,
|
user_uuid,
|
||||||
contents,
|
contents,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import Image from 'next/image'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import learnhousetextlogo from '../../../../public/learnhouse_logo.png'
|
import learnhousetextlogo from '../../../../public/learnhouse_logo.png'
|
||||||
import learnhouseiconlogo from '../../../../public/learnhouse_bigicon.png'
|
import learnhouseiconlogo from '../../../../public/learnhouse_bigicon.png'
|
||||||
import { BookCopy, School, Settings } from 'lucide-react'
|
import { BookCopy, School, Settings, Users } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
function DashboardHome() {
|
function DashboardHome() {
|
||||||
|
|
@ -12,33 +12,46 @@ function DashboardHome() {
|
||||||
<Image alt='learnhouse logo' width={230} src={learnhousetextlogo}></Image>
|
<Image alt='learnhouse logo' width={230} src={learnhousetextlogo}></Image>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-10'>
|
<div className='flex space-x-10'>
|
||||||
<Link href={`/dash/courses`} className='flex bg-white shadow-lg p-[35px] w-[350px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link href={`/dash/courses`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||||
<div className='flex flex-col mx-auto space-y-2'>
|
<div className='flex flex-col mx-auto space-y-2'>
|
||||||
<BookCopy className='mx-auto text-gray-500' size={50}></BookCopy>
|
<BookCopy className='mx-auto text-gray-500' size={50}></BookCopy>
|
||||||
<div className='text-center font-bold text-gray-500'>Courses</div>
|
<div className='text-center font-bold text-gray-500'>Courses</div>
|
||||||
<p className='text-center text-sm text-gray-400'>Create and manage courses, chapters and ativities </p>
|
<p className='text-center text-sm text-gray-400'>Create and manage courses, chapters and ativities </p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/dash/org/settings/general`} className='flex bg-white shadow-lg p-[35px] w-[350px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link href={`/dash/org/settings/general`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||||
<div className='flex flex-col mx-auto space-y-2'>
|
<div className='flex flex-col mx-auto space-y-2'>
|
||||||
<School className='mx-auto text-gray-500' size={50}></School>
|
<School className='mx-auto text-gray-500' size={50}></School>
|
||||||
<div className='text-center font-bold text-gray-500'>Organization</div>
|
<div className='text-center font-bold text-gray-500'>Organization</div>
|
||||||
<p className='text-center text-sm text-gray-400'>Configure your Organization general settings </p>
|
<p className='text-center text-sm text-gray-400'>Configure your Organization general settings </p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={'/dash/user/settings/general'} className='flex bg-white shadow-lg p-[35px] w-[350px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link href={`/dash/users/settings/users`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||||
<div className='flex flex-col mx-auto space-y-2'>
|
<div className='flex flex-col mx-auto space-y-2'>
|
||||||
<Settings className='mx-auto text-gray-500' size={50}></Settings>
|
<Users className='mx-auto text-gray-500' size={50}></Users>
|
||||||
<div className='text-center font-bold text-gray-500'>Personal Settings</div>
|
<div className='text-center font-bold text-gray-500'>Users</div>
|
||||||
<p className='text-center text-sm text-gray-400'>Configure your personal settings, passwords, email</p>
|
<p className='text-center text-sm text-gray-400'>Manage your Organization's users, roles </p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col space-y-10 '>
|
||||||
|
<div className='h-1 w-[100px] bg-neutral-200 rounded-full mx-auto'></div>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Link href={'https://learn.learnhouse.io/'} className='flex mt-[40px] bg-black space-x-2 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||||
|
<BookCopy className=' text-gray-100' size={20}></BookCopy>
|
||||||
|
<div className=' text-sm font-bold text-gray-100'>Learn LearnHouse</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className='mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full'></div>
|
||||||
|
<Link href={'/dash/user-account/settings/general'} className='flex bg-white shadow-lg p-[15px] items-center rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||||
|
<div className='flex flex-row mx-auto space-x-3 items-center'>
|
||||||
|
<Settings className=' text-gray-500' size={20}></Settings>
|
||||||
|
<div className=' font-bold text-gray-500'>Account Settings</div>
|
||||||
|
<p className=' text-sm text-gray-400'>Configure your personal settings, passwords, email</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-[80px] h-1 w-[100px] bg-neutral-200 rounded-full'></div>
|
|
||||||
<Link href={'https://learn.learnhouse.io/'} className='flex mt-[40px] bg-black space-x-4 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer'>
|
|
||||||
<BookCopy className='mx-auto text-gray-100' size={20}></BookCopy>
|
|
||||||
<div className='text-center text-sm font-bold text-gray-100'>Learn LearnHouse</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import UserEditGeneral from '@components/Dashboard/User/UserEditGeneral/UserEditGeneral';
|
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
|
||||||
import UserEditPassword from '@components/Dashboard/User/UserEditPassword/UserEditPassword';
|
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
import { getUriWithOrg } from '@services/config/config';
|
||||||
import { Info, Lock } from 'lucide-react';
|
import { Info, Lock } from 'lucide-react';
|
||||||
|
|
@ -24,7 +24,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full bg-[#f8f8f8]'>
|
<div className='h-full w-full bg-[#f8f8f8]'>
|
||||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
|
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
||||||
<BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs>
|
<BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs>
|
||||||
<div className='my-2 tracking-tighter'>
|
<div className='my-2 tracking-tighter'>
|
||||||
<div className='w-100 flex justify-between'>
|
<div className='w-100 flex justify-between'>
|
||||||
|
|
@ -32,7 +32,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-5 font-black text-sm'>
|
<div className='flex space-x-5 font-black text-sm'>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/general`}>
|
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/general`}>
|
||||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
||||||
|
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
<div className='flex items-center space-x-2.5 mx-2'>
|
||||||
|
|
@ -41,7 +41,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/security`}>
|
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/security`}>
|
||||||
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
<div className='flex items-center space-x-2.5 mx-2'>
|
||||||
<Lock size={16} />
|
<Lock size={16} />
|
||||||
|
|
@ -58,6 +58,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
||||||
|
className='h-full overflow-y-auto'
|
||||||
>
|
>
|
||||||
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
|
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
|
||||||
{params.subpage == 'security' ? <UserEditPassword /> : ''}
|
{params.subpage == 'security' ? <UserEditPassword /> : ''}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
'use client';
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
|
||||||
|
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { getUriWithOrg } from '@services/config/config';
|
||||||
|
import { Info, Lock, User, Users } from 'lucide-react';
|
||||||
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
|
||||||
|
import { useSession } from '@components/Contexts/SessionContext';
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers';
|
||||||
|
|
||||||
|
export type SettingsParams = {
|
||||||
|
subpage: string
|
||||||
|
orgslug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
|
const session = useSession() as any;
|
||||||
|
const org = useOrg() as any;
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
}
|
||||||
|
, [session, org])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
|
||||||
|
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
||||||
|
<BreadCrumbs type='org' last_breadcrumb='User settings' ></BreadCrumbs>
|
||||||
|
<div className='my-2 tracking-tighter'>
|
||||||
|
<div className='w-100 flex justify-between'>
|
||||||
|
<div className='pt-3 flex font-bold text-4xl'>Organization Users Settings</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex space-x-5 font-black text-sm'>
|
||||||
|
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/users`}>
|
||||||
|
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'users' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
||||||
|
|
||||||
|
<div className='flex items-center space-x-2.5 mx-2'>
|
||||||
|
<Users size={16} />
|
||||||
|
<div>Users</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
||||||
|
className='h-full overflow-y-auto'
|
||||||
|
>
|
||||||
|
{params.subpage == 'users' ? <OrgUsers /> : ''}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersSettingsPage
|
||||||
|
|
@ -62,7 +62,7 @@ function ThumbnailUpdate() {
|
||||||
<div className='flex justify-center items-center'>
|
<div className='flex justify-center items-center'>
|
||||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||||
<button
|
<button
|
||||||
className='font-bold antialiased items-center bg-gray-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex'
|
||||||
onClick={() => document.getElementById('fileInput')?.click()}
|
onClick={() => document.getElementById('fileInput')?.click()}
|
||||||
>
|
>
|
||||||
<UploadCloud size={16} className='mr-2' />
|
<UploadCloud size={16} className='mr-2' />
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ function BreadCrumbs(props: BreadCrumbsProps) {
|
||||||
<div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
|
<div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
|
||||||
<div className='flex items-center space-x-1'>
|
<div className='flex items-center space-x-1'>
|
||||||
{props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''}
|
{props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''}
|
||||||
{props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user/settings/general'>Account Settings</Link></div> : ''}
|
{props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user-account/settings/general'>Account Settings</Link></div> : ''}
|
||||||
{props.type == 'org' ? <div className='flex space-x-2 items-center'> <School className='text-gray' size={14}></School><Link href='/dash/users'>Organization Settings</Link></div> : ''}
|
{props.type == 'org' ? <div className='flex space-x-2 items-center'> <School className='text-gray' size={14}></School><Link href='/dash/users'>Organization Settings</Link></div> : ''}
|
||||||
<div className='flex items-center space-x-1 first-letter:uppercase'>
|
<div className='flex items-center space-x-1 first-letter:uppercase'>
|
||||||
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useSession } from '@components/Contexts/SessionContext';
|
||||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||||
import LearnHouseDashboardLogo from '@public/dashLogo.png';
|
import LearnHouseDashboardLogo from '@public/dashLogo.png';
|
||||||
import { logout } from '@services/auth/auth';
|
import { logout } from '@services/auth/auth';
|
||||||
import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings } from 'lucide-react'
|
import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
@ -65,6 +65,9 @@ function LeftMenu() {
|
||||||
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link>
|
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
<ToolTip content={"Users"} slateBlack sideOffset={8} side='right' >
|
||||||
|
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/users/settings/users`} ><Users size={18} /></Link>
|
||||||
|
</ToolTip>
|
||||||
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' >
|
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' >
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link>
|
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
|
@ -79,7 +82,7 @@ function LeftMenu() {
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<div className='flex items-center flex-col space-y-1'>
|
<div className='flex items-center flex-col space-y-1'>
|
||||||
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
||||||
<Link href={'/dash/user/settings/general'} className='py-3'>
|
<Link href={'/dash/user-account/settings/general'} className='py-3'>
|
||||||
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
|
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
|
||||||
</Link>
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
|
|
||||||
116
apps/web/components/Dashboard/Users/OrgUsers/OrgUsers.tsx
Normal file
116
apps/web/components/Dashboard/Users/OrgUsers/OrgUsers.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||||
|
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate';
|
||||||
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||||
|
import Modal from '@components/StyledElements/Modal/Modal';
|
||||||
|
import Toast from '@components/StyledElements/Toast/Toast';
|
||||||
|
import { getAPIUrl } from '@services/config/config';
|
||||||
|
import { removeUserFromOrg } from '@services/organizations/orgs';
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
|
import { KeyRound, LogOut, X } from 'lucide-react';
|
||||||
|
import React, { use, useEffect } from 'react'
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
|
||||||
|
function OrgUsers() {
|
||||||
|
const org = useOrg() as any;
|
||||||
|
const { data: orgUsers } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/users` : null, swrFetcher);
|
||||||
|
const [rolesModal, setRolesModal] = React.useState(false);
|
||||||
|
const [selectedUser, setSelectedUser] = React.useState(null) as any;
|
||||||
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
|
|
||||||
|
const handleRolesModal = (user_uuid: any) => {
|
||||||
|
setSelectedUser(user_uuid);
|
||||||
|
setRolesModal(!rolesModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveUser = async (user_id: any) => {
|
||||||
|
const res = await removeUserFromOrg(org.id, user_id);
|
||||||
|
if (res.status === 200) {
|
||||||
|
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (orgUsers) {
|
||||||
|
setIsLoading(false)
|
||||||
|
console.log(orgUsers)
|
||||||
|
}
|
||||||
|
}, [org, orgUsers])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading ? <div><PageLoading /></div> :
|
||||||
|
|
||||||
|
<>
|
||||||
|
<Toast></Toast>
|
||||||
|
<div className="h-6"></div>
|
||||||
|
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 '>
|
||||||
|
<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'>Role</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'>
|
||||||
|
<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'>{user.role.name}</td>
|
||||||
|
<td className='py-3 px-4 flex space-x-2 items-end'>
|
||||||
|
<Modal
|
||||||
|
isDialogOpen={rolesModal && selectedUser === user.user.user_uuid}
|
||||||
|
onOpenChange={() => handleRolesModal(user.user.user_uuid)}
|
||||||
|
minHeight="no-min"
|
||||||
|
dialogContent={
|
||||||
|
<RolesUpdate
|
||||||
|
alreadyAssignedRole={user.role.role_uuid}
|
||||||
|
setRolesModal={setRolesModal}
|
||||||
|
user={user} />
|
||||||
|
}
|
||||||
|
dialogTitle="Update Role"
|
||||||
|
dialogDescription={"Update @" + user.user.username + "'s role"}
|
||||||
|
dialogTrigger={
|
||||||
|
<button className='flex space-x-2 hover:cursor-pointer p-1 px-3 bg-yellow-700 rounded-md font-bold items-center text-sm text-yellow-100'>
|
||||||
|
<KeyRound className='w-4 h-4' />
|
||||||
|
<span> Edit Role</span>
|
||||||
|
</button>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText='Remove User'
|
||||||
|
confirmationMessage='Are you sure you want remove this user from the organization?'
|
||||||
|
dialogTitle={'Delete ' + user.user.username + ' ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<button className='mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100'>
|
||||||
|
<LogOut className='w-4 h-4' />
|
||||||
|
<span> Remove from organization</span>
|
||||||
|
</button>}
|
||||||
|
functionToExecute={() => { handleRemoveUser(user.user.id) }}
|
||||||
|
status='warning'
|
||||||
|
></ConfirmationModal>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgUsers
|
||||||
|
|
@ -126,7 +126,7 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
|
||||||
<FormField name="course-visibility">
|
<FormField name="course-visibility">
|
||||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||||
<FormLabel>Course Visibility</FormLabel>
|
<FormLabel>Course Visibility</FormLabel>
|
||||||
<FormMessage match="valueMissing">Please choose cours visibility</FormMessage>
|
<FormMessage match="valueMissing">Please choose course visibility</FormMessage>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<select onChange={handleVisibilityChange} className='border border-gray-300 rounded-md p-2' required>
|
<select onChange={handleVisibilityChange} className='border border-gray-300 rounded-md p-2' required>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
'use client';
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, Input, Textarea } from '@components/StyledElements/Form/Form'
|
||||||
|
import * as Form from '@radix-ui/react-form'
|
||||||
|
import { FormMessage } from "@radix-ui/react-form";
|
||||||
|
import { getAPIUrl } from '@services/config/config';
|
||||||
|
import { updateUserRole } from '@services/organizations/orgs';
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { BarLoader } from 'react-spinners';
|
||||||
|
import { mutate } from 'swr';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: any
|
||||||
|
setRolesModal: any
|
||||||
|
alreadyAssignedRole: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function RolesUpdate(props: Props) {
|
||||||
|
const org = useOrg() as any;
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
|
const [assignedRole, setAssignedRole] = React.useState(props.alreadyAssignedRole);
|
||||||
|
const [error, setError] = React.useState(null) as any;
|
||||||
|
|
||||||
|
const handleAssignedRole = (event: React.ChangeEvent<any>) => {
|
||||||
|
setError(null);
|
||||||
|
setAssignedRole(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
const res = await updateUserRole(org.id, props.user.user.id, assignedRole);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
|
||||||
|
props.setRolesModal(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setError('Error ' + res.status + ': ' + res.data.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
}
|
||||||
|
, [assignedRole])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormLayout onSubmit={handleSubmit}>
|
||||||
|
<FormField name="course-visibility">
|
||||||
|
{error ? <div className='text-red-500 font-bold text-xs px-3 py-2 bg-red-100 rounded-md'>{error}</div> : ''}
|
||||||
|
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||||
|
<FormLabel>Roles</FormLabel>
|
||||||
|
<FormMessage match="valueMissing">Please choose a role for the user</FormMessage>
|
||||||
|
</Flex>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<select onChange={handleAssignedRole} defaultValue={assignedRole} className='border border-gray-300 rounded-md p-2' required>
|
||||||
|
<option value="role_global_admin">Admin </option>
|
||||||
|
<option value="role_global_maintainer">Maintainer</option>
|
||||||
|
<option value="role_global_user">User</option>
|
||||||
|
</select>
|
||||||
|
</Form.Control>
|
||||||
|
</FormField>
|
||||||
|
<div className='h-full'></div>
|
||||||
|
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||||
|
<Form.Submit asChild>
|
||||||
|
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||||
|
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
||||||
|
: "Update user role"}
|
||||||
|
</ButtonBlack>
|
||||||
|
</Form.Submit>
|
||||||
|
</Flex>
|
||||||
|
</FormLayout>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RolesUpdate
|
||||||
|
|
@ -5,7 +5,7 @@ import { blackA, violet, mauve } from '@radix-ui/colors';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
const FormLayout = (props: any, onSubmit: any) => (
|
const FormLayout = (props: any, onSubmit: any) => (
|
||||||
<FormRoot onSubmit={props.onSubmit}>
|
<FormRoot className='h-fit' onSubmit={props.onSubmit}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</FormRoot>
|
</FormRoot>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getAPIUrl } from "@services/config/config";
|
import { getAPIUrl } from "@services/config/config";
|
||||||
import { RequestBody, errorHandling } from "@services/utils/ts/requests";
|
import { RequestBody, errorHandling, getResponseMetadata } from "@services/utils/ts/requests";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This file includes only POST, PUT, DELETE requests
|
This file includes only POST, PUT, DELETE requests
|
||||||
|
|
@ -49,3 +49,15 @@ export function getOrganizationContextInfoNoAsync(org_slug: any, next: any) {
|
||||||
const result = fetch(`${getAPIUrl()}orgs/slug/${org_slug}`, RequestBody("GET", null, next));
|
const result = fetch(`${getAPIUrl()}orgs/slug/${org_slug}`, RequestBody("GET", null, next));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateUserRole(org_id: any, user_id: any, role_uuid: any) {
|
||||||
|
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/users/${user_id}/role/${role_uuid}`, RequestBody("PUT", null, null));
|
||||||
|
const res = await getResponseMetadata(result);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeUserFromOrg(org_id: any, user_id: any) {
|
||||||
|
const result = await fetch(`${getAPIUrl()}orgs/${org_id}/users/${user_id}`, RequestBody("DELETE", null, null));
|
||||||
|
const res = await getResponseMetadata(result);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,15 @@ export const errorHandling = (res: any) => {
|
||||||
return res.json();
|
return res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getResponseMetadata = async (fetch_result: any) => {
|
|
||||||
|
type CustomResponseTyping = {
|
||||||
|
success: boolean;
|
||||||
|
data: any;
|
||||||
|
status: number;
|
||||||
|
HTTPmessage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResponseMetadata = async (fetch_result: any): Promise<CustomResponseTyping> => {
|
||||||
const json = await fetch_result.json();
|
const json = await fetch_result.json();
|
||||||
if (fetch_result.status === 200) {
|
if (fetch_result.status === 200) {
|
||||||
return { success: true, data: json, status: fetch_result.status, HTTPmessage: fetch_result.statusText };
|
return { success: true, data: json, status: fetch_result.status, HTTPmessage: fetch_result.statusText };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue