mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: enhance role management API with organization-specific role creation and retrieval, including comprehensive RBAC checks for permissions
This commit is contained in:
parent
3ce019abec
commit
531e1863c0
10 changed files with 2174 additions and 32 deletions
|
|
@ -1,28 +1,45 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException
|
||||
from sqlmodel import Session
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.roles import RoleCreate, RoleRead, RoleUpdate
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.roles.roles import create_role, delete_role, read_role, update_role
|
||||
from src.services.roles.roles import create_role, delete_role, read_role, update_role, get_roles_by_organization
|
||||
from src.db.users import PublicUser
|
||||
from typing import List
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
@router.post("/org/{org_id}")
|
||||
async def api_create_role(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
role_object: RoleCreate,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
)-> RoleRead:
|
||||
"""
|
||||
Create new role
|
||||
Create new role for a specific organization
|
||||
"""
|
||||
# Set the org_id in the role object
|
||||
role_object.org_id = org_id
|
||||
return await create_role(request, db_session, role_object, current_user)
|
||||
|
||||
|
||||
@router.get("/org/{org_id}")
|
||||
async def api_get_roles_by_organization(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
)-> List[RoleRead]:
|
||||
"""
|
||||
Get all roles for a specific organization, including global roles
|
||||
"""
|
||||
return await get_roles_by_organization(request, db_session, org_id, current_user)
|
||||
|
||||
|
||||
@router.get("/{role_id}")
|
||||
async def api_get_role(
|
||||
request: Request,
|
||||
|
|
@ -39,6 +56,7 @@ async def api_get_role(
|
|||
@router.put("/{role_id}")
|
||||
async def api_update_role(
|
||||
request: Request,
|
||||
role_id: str,
|
||||
role_object: RoleUpdate,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
|
|
@ -46,6 +64,16 @@ async def api_update_role(
|
|||
"""
|
||||
Update role by role_id
|
||||
"""
|
||||
# Convert role_id to integer and set it in the role_object
|
||||
try:
|
||||
role_id_int = int(role_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid role ID format. Role ID must be a number.",
|
||||
)
|
||||
|
||||
role_object.role_id = role_id_int
|
||||
return await update_role(request, db_session, role_object, current_user)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
from typing import Literal
|
||||
from typing import Literal, List
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session, select
|
||||
from sqlmodel import Session, select, text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.db.roles import Role, RoleCreate, RoleRead, RoleUpdate
|
||||
from src.db.roles import Role, RoleCreate, RoleRead, RoleUpdate, RoleTypeEnum
|
||||
from src.db.organizations import Organization
|
||||
from src.db.user_organizations import UserOrganization
|
||||
from fastapi import HTTPException, Request
|
||||
from datetime import datetime
|
||||
|
||||
|
|
@ -22,24 +25,401 @@ async def create_role(
|
|||
# RBAC check
|
||||
await rbac_check(request, current_user, "create", "role_xxx", db_session)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 1: Ensure the role is created as TYPE_ORGANIZATION and has an org_id
|
||||
# ============================================================================
|
||||
if not role.org_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Organization ID is required for role creation",
|
||||
)
|
||||
|
||||
# Force the role type to be TYPE_ORGANIZATION for user-created roles
|
||||
role.role_type = RoleTypeEnum.TYPE_ORGANIZATION
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 2: Check if the organization exists
|
||||
# ============================================================================
|
||||
statement = select(Organization).where(Organization.id == role.org_id)
|
||||
organization = db_session.exec(statement).first()
|
||||
|
||||
if not organization:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 3: Check if the current user is a member of the organization
|
||||
# ============================================================================
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == current_user.id,
|
||||
UserOrganization.org_id == role.org_id
|
||||
)
|
||||
user_org = db_session.exec(statement).first()
|
||||
|
||||
if not user_org:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You are not a member of this organization",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 4: Check if the user has permission to create roles in this organization
|
||||
# ============================================================================
|
||||
# Get the user's role in this organization
|
||||
statement = select(Role).where(Role.id == user_org.role_id)
|
||||
user_role = db_session.exec(statement).first()
|
||||
|
||||
if not user_role:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Your role in this organization could not be determined",
|
||||
)
|
||||
|
||||
# Check if the user has role creation permissions
|
||||
if user_role.rights and isinstance(user_role.rights, dict):
|
||||
roles_rights = user_role.rights.get('roles', {})
|
||||
if not roles_rights.get('action_create', False):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to create roles in this organization",
|
||||
)
|
||||
else:
|
||||
# If no rights are defined, check if user has admin role (role_id 1 or 2)
|
||||
if user_role.id not in [1, 2]: # Admin and Maintainer roles
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to create roles in this organization. Admin or Maintainer role required.",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 5: Check if a role with the same name already exists in this organization
|
||||
# ============================================================================
|
||||
statement = select(Role).where(
|
||||
Role.name == role.name,
|
||||
Role.org_id == role.org_id,
|
||||
Role.role_type == RoleTypeEnum.TYPE_ORGANIZATION
|
||||
)
|
||||
existing_role = db_session.exec(statement).first()
|
||||
|
||||
if existing_role:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"A role with the name '{role.name}' already exists in this organization",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 6: Validate role name and description
|
||||
# ============================================================================
|
||||
if not role.name or role.name.strip() == "":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Role name is required and cannot be empty",
|
||||
)
|
||||
|
||||
if len(role.name.strip()) > 100: # Assuming a reasonable limit
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Role name cannot exceed 100 characters",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 7: Validate rights structure if provided
|
||||
# ============================================================================
|
||||
if role.rights:
|
||||
# Convert Rights model to dict if needed
|
||||
if isinstance(role.rights, dict):
|
||||
# It's already a dict
|
||||
rights_dict = role.rights
|
||||
else:
|
||||
# It's likely a Pydantic model, try to convert to dict
|
||||
try:
|
||||
# Try dict() method first (for Pydantic v1)
|
||||
rights_dict = role.rights.dict()
|
||||
except AttributeError:
|
||||
try:
|
||||
# Try model_dump() method (for Pydantic v2)
|
||||
rights_dict = role.rights.model_dump() # type: ignore
|
||||
except AttributeError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Rights must be provided as a JSON object",
|
||||
)
|
||||
|
||||
# Validate rights structure - check for required top-level keys
|
||||
required_rights = [
|
||||
'courses', 'users', 'usergroups', 'collections',
|
||||
'organizations', 'coursechapters', 'activities',
|
||||
'roles', 'dashboard'
|
||||
]
|
||||
|
||||
for required_right in required_rights:
|
||||
if required_right not in rights_dict:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Missing required right: {required_right}",
|
||||
)
|
||||
|
||||
# Validate the structure of each right
|
||||
right_data = rights_dict[required_right]
|
||||
if not isinstance(right_data, dict):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Right '{required_right}' must be a JSON object",
|
||||
)
|
||||
|
||||
# Validate courses permissions (has additional 'own' permissions)
|
||||
if required_right == 'courses':
|
||||
required_course_permissions = [
|
||||
'action_create', 'action_read', 'action_read_own',
|
||||
'action_update', 'action_update_own', 'action_delete', 'action_delete_own'
|
||||
]
|
||||
for perm in required_course_permissions:
|
||||
if perm not in right_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Missing required course permission: {perm}",
|
||||
)
|
||||
if not isinstance(right_data[perm], bool):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Course permission '{perm}' must be a boolean",
|
||||
)
|
||||
|
||||
# Validate other permissions (standard permissions)
|
||||
elif required_right in ['users', 'usergroups', 'collections', 'organizations', 'coursechapters', 'activities', 'roles']:
|
||||
required_permissions = ['action_create', 'action_read', 'action_update', 'action_delete']
|
||||
for perm in required_permissions:
|
||||
if perm not in right_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Missing required permission '{perm}' for '{required_right}'",
|
||||
)
|
||||
if not isinstance(right_data[perm], bool):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Permission '{perm}' for '{required_right}' must be a boolean",
|
||||
)
|
||||
|
||||
# Validate dashboard permissions
|
||||
elif required_right == 'dashboard':
|
||||
if 'action_access' not in right_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Missing required dashboard permission: action_access",
|
||||
)
|
||||
if not isinstance(right_data['action_access'], bool):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Dashboard permission 'action_access' must be a boolean",
|
||||
)
|
||||
|
||||
# Convert back to dict if it was a model
|
||||
if not isinstance(role.rights, dict):
|
||||
role.rights = rights_dict
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 8: Ensure user cannot create a role with higher permissions than they have
|
||||
# ============================================================================
|
||||
if role.rights and isinstance(role.rights, dict) and user_role.rights and isinstance(user_role.rights, dict):
|
||||
# Check if the new role has any permissions that the user doesn't have
|
||||
for right_key, right_permissions in role.rights.items():
|
||||
if right_key in user_role.rights:
|
||||
user_right_permissions = user_role.rights[right_key]
|
||||
|
||||
# Check each permission in the right
|
||||
for perm_key, perm_value in right_permissions.items():
|
||||
if isinstance(perm_value, bool) and perm_value: # If the new role has this permission enabled
|
||||
if isinstance(user_right_permissions, dict) and perm_key in user_right_permissions:
|
||||
user_has_perm = user_right_permissions[perm_key]
|
||||
if not user_has_perm:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"You cannot create a role with '{perm_key}' permission for '{right_key}' as you don't have this permission yourself",
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"You cannot create a role with '{perm_key}' permission for '{right_key}' as you don't have this permission yourself",
|
||||
)
|
||||
|
||||
# Complete the role object
|
||||
role.role_uuid = f"role_{uuid4()}"
|
||||
role.creation_date = str(datetime.now())
|
||||
role.update_date = str(datetime.now())
|
||||
|
||||
db_session.add(role)
|
||||
db_session.commit()
|
||||
db_session.refresh(role)
|
||||
# ============================================================================
|
||||
# VERIFICATION 9: Handle ID sequence issue (existing logic)
|
||||
# ============================================================================
|
||||
try:
|
||||
db_session.add(role)
|
||||
db_session.commit()
|
||||
db_session.refresh(role)
|
||||
except IntegrityError as e:
|
||||
if "duplicate key value violates unique constraint" in str(e) and "role_pkey" in str(e):
|
||||
# Handle the sequence issue by finding the next available ID
|
||||
db_session.rollback()
|
||||
|
||||
# Get the maximum ID from the role table using raw SQL
|
||||
result = db_session.execute(text("SELECT COALESCE(MAX(id), 0) as max_id FROM role"))
|
||||
max_id_result = result.scalar()
|
||||
max_id = max_id_result if max_id_result is not None else 0
|
||||
|
||||
# Set the next available ID
|
||||
role.id = max_id + 1
|
||||
|
||||
# Try to insert again
|
||||
db_session.add(role)
|
||||
db_session.commit()
|
||||
db_session.refresh(role)
|
||||
|
||||
# Update the sequence to the correct value for future inserts
|
||||
try:
|
||||
# Use raw SQL to update the sequence
|
||||
db_session.execute(text(f"SELECT setval('role_id_seq', {max_id + 1}, true)"))
|
||||
db_session.commit()
|
||||
except Exception:
|
||||
# If sequence doesn't exist or can't be updated, that's okay
|
||||
# The manual ID assignment above will handle it
|
||||
pass
|
||||
else:
|
||||
# Re-raise the original exception if it's not the sequence issue
|
||||
raise e
|
||||
|
||||
role = RoleRead(**role.model_dump())
|
||||
# Create RoleRead object with all required fields
|
||||
role_data = role.model_dump()
|
||||
# Ensure org_id is properly handled
|
||||
if role_data.get('org_id') is None:
|
||||
role_data['org_id'] = 0
|
||||
role = RoleRead(**role_data)
|
||||
|
||||
return role
|
||||
|
||||
|
||||
async def get_roles_by_organization(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
org_id: int,
|
||||
current_user: PublicUser,
|
||||
) -> List[RoleRead]:
|
||||
"""
|
||||
Get all roles for a specific organization, including global roles.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
db_session: Database session
|
||||
org_id: Organization ID
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
List[RoleRead]: List of roles for the organization (including global roles)
|
||||
|
||||
Raises:
|
||||
HTTPException: If organization not found or user lacks permissions
|
||||
"""
|
||||
# ============================================================================
|
||||
# VERIFICATION 1: Check if the organization exists
|
||||
# ============================================================================
|
||||
statement = select(Organization).where(Organization.id == org_id)
|
||||
organization = db_session.exec(statement).first()
|
||||
|
||||
if not organization:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 2: Check if the current user is a member of the organization
|
||||
# ============================================================================
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == current_user.id,
|
||||
UserOrganization.org_id == org_id
|
||||
)
|
||||
user_org = db_session.exec(statement).first()
|
||||
|
||||
if not user_org:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You are not a member of this organization",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION 3: Check if the user has permission to read roles in this organization
|
||||
# ============================================================================
|
||||
# Get the user's role in this organization
|
||||
statement = select(Role).where(Role.id == user_org.role_id)
|
||||
user_role = db_session.exec(statement).first()
|
||||
|
||||
if not user_role:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Your role in this organization could not be determined",
|
||||
)
|
||||
|
||||
# Check if the user has role reading permissions
|
||||
if user_role.rights and isinstance(user_role.rights, dict):
|
||||
roles_rights = user_role.rights.get('roles', {})
|
||||
if not roles_rights.get('action_read', False):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to read roles in this organization",
|
||||
)
|
||||
else:
|
||||
# If no rights are defined, check if user has admin role (role_id 1 or 2)
|
||||
if user_role.id not in [1, 2]: # Admin and Maintainer roles
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to read roles in this organization. Admin or Maintainer role required.",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# GET ROLES: Fetch all roles for the organization AND global roles
|
||||
# ============================================================================
|
||||
# Get global roles first
|
||||
global_roles_statement = select(Role).where(
|
||||
Role.role_type == RoleTypeEnum.TYPE_GLOBAL
|
||||
).order_by(Role.id) # type: ignore
|
||||
|
||||
global_roles = list(db_session.exec(global_roles_statement).all())
|
||||
|
||||
# Get organization-specific roles
|
||||
org_roles_statement = select(Role).where(
|
||||
Role.org_id == org_id,
|
||||
Role.role_type == RoleTypeEnum.TYPE_ORGANIZATION
|
||||
).order_by(Role.id) # type: ignore
|
||||
|
||||
org_roles = list(db_session.exec(org_roles_statement).all())
|
||||
|
||||
# Combine lists with global roles first, then organization roles
|
||||
all_roles = global_roles + org_roles
|
||||
|
||||
# Convert to RoleRead objects
|
||||
role_reads = []
|
||||
for role in all_roles:
|
||||
role_data = role.model_dump()
|
||||
# Ensure org_id is properly handled
|
||||
if role_data.get('org_id') is None:
|
||||
role_data['org_id'] = 0
|
||||
role_reads.append(RoleRead(**role_data))
|
||||
|
||||
return role_reads
|
||||
|
||||
|
||||
async def read_role(
|
||||
request: Request, db_session: Session, role_id: str, current_user: PublicUser
|
||||
):
|
||||
statement = select(Role).where(Role.id == role_id)
|
||||
# Convert role_id to integer
|
||||
try:
|
||||
role_id_int = int(role_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid role ID format. Role ID must be a number.",
|
||||
)
|
||||
|
||||
statement = select(Role).where(Role.id == role_id_int)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
role = result.first()
|
||||
|
|
@ -75,6 +455,15 @@ async def update_role(
|
|||
detail="Role not found",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION: Prevent updating TYPE_GLOBAL roles
|
||||
# ============================================================================
|
||||
if role.role_type == RoleTypeEnum.TYPE_GLOBAL:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Global roles cannot be updated. These are system-defined roles that must remain unchanged.",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, current_user, "update", role.role_uuid, db_session)
|
||||
|
||||
|
|
@ -85,9 +474,116 @@ async def update_role(
|
|||
del role_object.role_id
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(role_object).items():
|
||||
# Use model_dump() to get the data as a dictionary
|
||||
try:
|
||||
update_data = role_object.model_dump(exclude_unset=True)
|
||||
except AttributeError:
|
||||
# Fallback to dict() method for older Pydantic versions
|
||||
try:
|
||||
update_data = role_object.dict(exclude_unset=True)
|
||||
except AttributeError:
|
||||
# Fallback to vars() for SQLModel
|
||||
update_data = {k: v for k, v in vars(role_object).items() if v is not None}
|
||||
|
||||
# Update the role with the new data
|
||||
for key, value in update_data.items():
|
||||
if value is not None:
|
||||
setattr(role, var, value)
|
||||
setattr(role, key, value)
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATE RIGHTS STRUCTURE if rights are being updated
|
||||
# ============================================================================
|
||||
if role.rights:
|
||||
# Convert Rights model to dict if needed
|
||||
if isinstance(role.rights, dict):
|
||||
# It's already a dict
|
||||
rights_dict = role.rights
|
||||
else:
|
||||
# It's likely a Pydantic model, try to convert to dict
|
||||
try:
|
||||
# Try dict() method first (for Pydantic v1)
|
||||
rights_dict = role.rights.dict()
|
||||
except AttributeError:
|
||||
try:
|
||||
# Try model_dump() method (for Pydantic v2)
|
||||
rights_dict = role.rights.model_dump() # type: ignore
|
||||
except AttributeError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Rights must be provided as a JSON object",
|
||||
)
|
||||
|
||||
# Validate rights structure - check for required top-level keys
|
||||
required_rights = [
|
||||
'courses', 'users', 'usergroups', 'collections',
|
||||
'organizations', 'coursechapters', 'activities',
|
||||
'roles', 'dashboard'
|
||||
]
|
||||
|
||||
for required_right in required_rights:
|
||||
if required_right not in rights_dict:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Missing required right: {required_right}",
|
||||
)
|
||||
|
||||
# Validate the structure of each right
|
||||
right_data = rights_dict[required_right]
|
||||
if not isinstance(right_data, dict):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Right '{required_right}' must be a JSON object",
|
||||
)
|
||||
|
||||
# Validate courses permissions (has additional 'own' permissions)
|
||||
if required_right == 'courses':
|
||||
required_course_permissions = [
|
||||
'action_create', 'action_read', 'action_read_own',
|
||||
'action_update', 'action_update_own', 'action_delete', 'action_delete_own'
|
||||
]
|
||||
for perm in required_course_permissions:
|
||||
if perm not in right_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Missing required course permission: {perm}",
|
||||
)
|
||||
if not isinstance(right_data[perm], bool):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Course permission '{perm}' must be a boolean",
|
||||
)
|
||||
|
||||
# Validate other permissions (standard permissions)
|
||||
elif required_right in ['users', 'usergroups', 'collections', 'organizations', 'coursechapters', 'activities', 'roles']:
|
||||
required_permissions = ['action_create', 'action_read', 'action_update', 'action_delete']
|
||||
for perm in required_permissions:
|
||||
if perm not in right_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Missing required permission '{perm}' for '{required_right}'",
|
||||
)
|
||||
if not isinstance(right_data[perm], bool):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Permission '{perm}' for '{required_right}' must be a boolean",
|
||||
)
|
||||
|
||||
# Validate dashboard permissions
|
||||
elif required_right == 'dashboard':
|
||||
if 'action_access' not in right_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Missing required dashboard permission: action_access",
|
||||
)
|
||||
if not isinstance(right_data['action_access'], bool):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Dashboard permission 'action_access' must be a boolean",
|
||||
)
|
||||
|
||||
# Convert back to dict if it was a model
|
||||
if not isinstance(role.rights, dict):
|
||||
role.rights = rights_dict
|
||||
|
||||
db_session.add(role)
|
||||
db_session.commit()
|
||||
|
|
@ -101,10 +597,17 @@ async def update_role(
|
|||
async def delete_role(
|
||||
request: Request, db_session: Session, role_id: str, current_user: PublicUser
|
||||
):
|
||||
# RBAC check
|
||||
await rbac_check(request, current_user, "delete", role_id, db_session)
|
||||
|
||||
statement = select(Role).where(Role.id == role_id)
|
||||
# Convert role_id to integer
|
||||
try:
|
||||
role_id_int = int(role_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid role ID format. Role ID must be a number.",
|
||||
)
|
||||
|
||||
# First, get the role to check if it exists and get its UUID
|
||||
statement = select(Role).where(Role.id == role_id_int)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
role = result.first()
|
||||
|
|
@ -115,6 +618,18 @@ async def delete_role(
|
|||
detail="Role not found",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VERIFICATION: Prevent deleting TYPE_GLOBAL roles
|
||||
# ============================================================================
|
||||
if role.role_type == RoleTypeEnum.TYPE_GLOBAL:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Global roles cannot be deleted. These are system-defined roles that must remain unchanged.",
|
||||
)
|
||||
|
||||
# RBAC check using the role's UUID
|
||||
await rbac_check(request, current_user, "delete", role.role_uuid, db_session)
|
||||
|
||||
db_session.delete(role)
|
||||
db_session.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { motion } from 'framer-motion'
|
|||
import Link from 'next/link'
|
||||
import { useMediaQuery } from 'usehooks-ts'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import { Monitor, ScanEye, SquareUserRound, UserPlus, Users } from 'lucide-react'
|
||||
import { Monitor, ScanEye, SquareUserRound, UserPlus, Users, Shield } from 'lucide-react'
|
||||
import BreadCrumbs from '@components/Dashboard/Misc/BreadCrumbs'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
|
|
@ -12,6 +12,7 @@ import OrgUsers from '@components/Dashboard/Pages/Users/OrgUsers/OrgUsers'
|
|||
import OrgAccess from '@components/Dashboard/Pages/Users/OrgAccess/OrgAccess'
|
||||
import OrgUsersAdd from '@components/Dashboard/Pages/Users/OrgUsersAdd/OrgUsersAdd'
|
||||
import OrgUserGroups from '@components/Dashboard/Pages/Users/OrgUserGroups/OrgUserGroups'
|
||||
import OrgRoles from '@components/Dashboard/Pages/Users/OrgRoles/OrgRoles'
|
||||
|
||||
export type SettingsParams = {
|
||||
subpage: string
|
||||
|
|
@ -43,6 +44,10 @@ function UsersSettingsPage(props: { params: Promise<SettingsParams> }) {
|
|||
setH1Label('UserGroups')
|
||||
setH2Label('Create and manage user groups')
|
||||
}
|
||||
if (params.subpage == 'roles') {
|
||||
setH1Label('Roles')
|
||||
setH2Label('Create and manage roles with specific permissions')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -112,6 +117,23 @@ function UsersSettingsPage(props: { params: Promise<SettingsParams> }) {
|
|||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/roles`
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'roles'
|
||||
? 'border-b-4'
|
||||
: 'opacity-50'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5 mx-2">
|
||||
<Shield size={16} />
|
||||
<div>Roles</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/signups`
|
||||
|
|
@ -160,6 +182,7 @@ function UsersSettingsPage(props: { params: Promise<SettingsParams> }) {
|
|||
{params.subpage == 'signups' ? <OrgAccess /> : ''}
|
||||
{params.subpage == 'add' ? <OrgUsersAdd /> : ''}
|
||||
{params.subpage == 'usergroups' ? <OrgUserGroups /> : ''}
|
||||
{params.subpage == 'roles' ? <OrgRoles /> : ''}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
295
apps/web/components/Dashboard/Pages/Users/OrgRoles/OrgRoles.tsx
Normal file
295
apps/web/components/Dashboard/Pages/Users/OrgRoles/OrgRoles.tsx
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
'use client'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import AddRole from '@components/Objects/Modals/Dash/OrgRoles/AddRole'
|
||||
import EditRole from '@components/Objects/Modals/Dash/OrgRoles/EditRole'
|
||||
import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import Modal from '@components/Objects/StyledElements/Modal/Modal'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { deleteRole } from '@services/roles/roles'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import { Pencil, Shield, Users, X, Globe } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
|
||||
function OrgRoles() {
|
||||
const org = useOrg() as any
|
||||
const session = useLHSession() as any
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const [createRoleModal, setCreateRoleModal] = React.useState(false)
|
||||
const [editRoleModal, setEditRoleModal] = React.useState(false)
|
||||
const [selectedRole, setSelectedRole] = React.useState(null) as any
|
||||
|
||||
const { data: roles } = useSWR(
|
||||
org ? `${getAPIUrl()}roles/org/${org.id}` : null,
|
||||
(url) => swrFetcher(url, access_token)
|
||||
)
|
||||
|
||||
const deleteRoleUI = async (role_id: any) => {
|
||||
const toastId = toast.loading("Deleting...");
|
||||
const res = await deleteRole(role_id, org.id, access_token)
|
||||
if (res.status === 200) {
|
||||
mutate(`${getAPIUrl()}roles/org/${org.id}`)
|
||||
toast.success("Deleted role", {id:toastId})
|
||||
}
|
||||
else {
|
||||
toast.error('Error deleting role', {id:toastId})
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditRoleModal = (role: any) => {
|
||||
setSelectedRole(role)
|
||||
setEditRoleModal(!editRoleModal)
|
||||
}
|
||||
|
||||
const getRightsSummary = (rights: any) => {
|
||||
if (!rights) return 'No permissions'
|
||||
|
||||
const totalPermissions = Object.keys(rights).reduce((acc, key) => {
|
||||
if (typeof rights[key] === 'object') {
|
||||
return acc + Object.keys(rights[key]).filter(k => rights[key][k] === true).length
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
return `${totalPermissions} permissions`
|
||||
}
|
||||
|
||||
// Check if a role is system-wide (TYPE_GLOBAL or role_uuid starts with role_global_)
|
||||
const isSystemRole = (role: any) => {
|
||||
// Check for role_type field first
|
||||
if (role.role_type === 'TYPE_GLOBAL') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for role_uuid starting with role_global_
|
||||
if (role.role_uuid && role.role_uuid.startsWith('role_global_')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for common system role IDs (1-4 are typically system roles)
|
||||
if (role.id && [1, 2, 3, 4].includes(role.id)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the role name indicates it's a system role
|
||||
if (role.name && ['Admin', 'Maintainer', 'Instructor', 'User'].includes(role.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-6"></div>
|
||||
<div className="mx-4 sm:mx-6 lg:mx-10 bg-white rounded-xl nice-shadow px-3 sm:px-4 py-4">
|
||||
<div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-md mb-3">
|
||||
<h1 className="font-bold text-lg sm:text-xl text-gray-800">Manage Roles & Permissions</h1>
|
||||
<h2 className="text-gray-500 text-xs sm:text-sm">
|
||||
{' '}
|
||||
Roles define what users can do within your organization. Create custom roles with specific permissions for different user types.{' '}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Mobile view - Cards */}
|
||||
<div className="block sm:hidden space-y-3">
|
||||
{roles?.map((role: any) => {
|
||||
const isSystem = isSystemRole(role)
|
||||
return (
|
||||
<div key={role.id} className="bg-white border border-gray-200 rounded-lg p-4 space-y-3 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Shield className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-medium text-sm">{role.name}</span>
|
||||
{isSystem && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<Globe className="w-3 h-3 mr-1" />
|
||||
System-wide
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{getRightsSummary(role.rights)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">{role.description || 'No description'}</p>
|
||||
<div className="flex space-x-2">
|
||||
{!isSystem ? (
|
||||
<>
|
||||
<Modal
|
||||
isDialogOpen={
|
||||
editRoleModal &&
|
||||
selectedRole?.id === role.id
|
||||
}
|
||||
onOpenChange={() =>
|
||||
handleEditRoleModal(role)
|
||||
}
|
||||
minHeight="lg"
|
||||
minWidth='xl'
|
||||
customWidth="max-w-7xl"
|
||||
dialogContent={
|
||||
<EditRole
|
||||
role={role}
|
||||
setEditRoleModal={setEditRoleModal}
|
||||
/>
|
||||
}
|
||||
dialogTitle="Edit Role"
|
||||
dialogDescription={
|
||||
'Edit the role permissions and details'
|
||||
}
|
||||
dialogTrigger={
|
||||
<button className="flex-1 flex justify-center space-x-2 hover:cursor-pointer p-2 bg-black rounded-md font-bold items-center text-sm text-white hover:bg-gray-800 transition-colors shadow-sm">
|
||||
<Pencil className="w-4 h-4" />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Role"
|
||||
confirmationMessage="This action cannot be undone. All users with this role will lose their permissions. Are you sure you want to delete this role?"
|
||||
dialogTitle={'Delete Role ?'}
|
||||
dialogTrigger={
|
||||
<button className="flex-1 flex justify-center space-x-2 hover:cursor-pointer p-2 bg-red-600 rounded-md font-bold items-center text-sm text-white hover:bg-red-700 transition-colors shadow-sm">
|
||||
<X className="w-4 h-4" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
}
|
||||
functionToExecute={() => {
|
||||
deleteRoleUI(role.id)
|
||||
}}
|
||||
status="warning"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Desktop view - Table */}
|
||||
<div className="hidden sm:block overflow-x-auto">
|
||||
<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">Role Name</th>
|
||||
<th className="py-3 px-4">Description</th>
|
||||
<th className="py-3 px-4">Permissions</th>
|
||||
<th className="py-3 px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<>
|
||||
<tbody className="mt-5 bg-white rounded-md">
|
||||
{roles?.map((role: any) => {
|
||||
const isSystem = isSystemRole(role)
|
||||
return (
|
||||
<tr key={role.id} className="border-b border-gray-100 text-sm hover:bg-gray-50 transition-colors">
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Shield className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-medium">{role.name}</span>
|
||||
{isSystem && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<Globe className="w-3 h-3 mr-1" />
|
||||
System-wide
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-gray-600">{role.description || 'No description'}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{getRightsSummary(role.rights)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex space-x-2">
|
||||
{!isSystem ? (
|
||||
<>
|
||||
<Modal
|
||||
isDialogOpen={
|
||||
editRoleModal &&
|
||||
selectedRole?.id === role.id
|
||||
}
|
||||
onOpenChange={() =>
|
||||
handleEditRoleModal(role)
|
||||
}
|
||||
minHeight="lg"
|
||||
minWidth='xl'
|
||||
customWidth="max-w-7xl"
|
||||
dialogContent={
|
||||
<EditRole
|
||||
role={role}
|
||||
setEditRoleModal={setEditRoleModal}
|
||||
/>
|
||||
}
|
||||
dialogTitle="Edit Role"
|
||||
dialogDescription={
|
||||
'Edit the role permissions and details'
|
||||
}
|
||||
dialogTrigger={
|
||||
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-black rounded-md font-bold items-center text-sm text-white hover:bg-gray-800 transition-colors shadow-sm">
|
||||
<Pencil className="w-4 h-4" />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Role"
|
||||
confirmationMessage="This action cannot be undone. All users with this role will lose their permissions. Are you sure you want to delete this role?"
|
||||
dialogTitle={'Delete Role ?'}
|
||||
dialogTrigger={
|
||||
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-red-600 rounded-md font-bold items-center text-sm text-white hover:bg-red-700 transition-colors shadow-sm">
|
||||
<X className="w-4 h-4" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
}
|
||||
functionToExecute={() => {
|
||||
deleteRoleUI(role.id)
|
||||
}}
|
||||
status="warning"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end mt-3 mr-2'>
|
||||
<Modal
|
||||
isDialogOpen={createRoleModal}
|
||||
onOpenChange={() => setCreateRoleModal(!createRoleModal)}
|
||||
minHeight="no-min"
|
||||
minWidth='xl'
|
||||
customWidth="max-w-7xl"
|
||||
dialogContent={
|
||||
<AddRole
|
||||
setCreateRoleModal={setCreateRoleModal}
|
||||
/>
|
||||
}
|
||||
dialogTitle="Create a Role"
|
||||
dialogDescription={
|
||||
'Create a new role with specific permissions'
|
||||
}
|
||||
dialogTrigger={
|
||||
<button className="flex space-x-2 hover:cursor-pointer p-2 sm:p-1 sm:px-3 bg-black rounded-md font-bold items-center text-sm text-white w-full sm:w-auto justify-center hover:bg-gray-800 transition-colors shadow-sm">
|
||||
<Shield className="w-4 h-4" />
|
||||
<span>Create a Role</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgRoles
|
||||
599
apps/web/components/Objects/Modals/Dash/OrgRoles/AddRole.tsx
Normal file
599
apps/web/components/Objects/Modals/Dash/OrgRoles/AddRole.tsx
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
'use client'
|
||||
import FormLayout, {
|
||||
FormField,
|
||||
FormLabelAndMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@components/Objects/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import React from 'react'
|
||||
import { createRole } from '@services/roles/roles'
|
||||
import { mutate } from 'swr'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { useFormik } from 'formik'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Shield, BookOpen, Users, UserCheck, FolderOpen, Building, FileText, Activity, Settings, Monitor, CheckSquare, Square } from 'lucide-react'
|
||||
|
||||
type AddRoleProps = {
|
||||
setCreateRoleModal: any
|
||||
}
|
||||
|
||||
interface Rights {
|
||||
courses: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_read_own: boolean;
|
||||
action_update: boolean;
|
||||
action_update_own: boolean;
|
||||
action_delete: boolean;
|
||||
action_delete_own: boolean;
|
||||
};
|
||||
users: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
usergroups: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
collections: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
organizations: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
coursechapters: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
activities: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
roles: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
dashboard: {
|
||||
action_access: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
|
||||
if (!values.name) {
|
||||
errors.name = 'Required'
|
||||
} else if (values.name.length < 2) {
|
||||
errors.name = 'Name must be at least 2 characters'
|
||||
}
|
||||
|
||||
if (!values.description) {
|
||||
errors.description = 'Required'
|
||||
} else if (values.description.length < 10) {
|
||||
errors.description = 'Description must be at least 10 characters'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const defaultRights: Rights = {
|
||||
courses: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_read_own: false,
|
||||
action_update: false,
|
||||
action_update_own: false,
|
||||
action_delete: false,
|
||||
action_delete_own: false
|
||||
},
|
||||
users: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
usergroups: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
collections: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
organizations: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
coursechapters: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
activities: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
roles: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
dashboard: {
|
||||
action_access: false
|
||||
}
|
||||
}
|
||||
|
||||
const predefinedRoles = {
|
||||
'Admin': {
|
||||
name: 'Admin',
|
||||
description: 'Full platform control with all permissions',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: true, action_update_own: true, action_delete: true, action_delete_own: true },
|
||||
users: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
usergroups: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
collections: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
organizations: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
activities: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
roles: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Course Manager': {
|
||||
name: 'Course Manager',
|
||||
description: 'Can manage courses, chapters, and activities',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: true, action_update_own: true, action_delete: false, action_delete_own: true },
|
||||
users: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
collections: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
activities: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Instructor': {
|
||||
name: 'Instructor',
|
||||
description: 'Can create and manage their own courses',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: false, action_update_own: true, action_delete: false, action_delete_own: true },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: true, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Viewer': {
|
||||
name: 'Viewer',
|
||||
description: 'Read-only access to courses and content',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Content Creator': {
|
||||
name: 'Content Creator',
|
||||
description: 'Can create and edit content but not manage users',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: true, action_update_own: true, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
activities: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'User Manager': {
|
||||
name: 'User Manager',
|
||||
description: 'Can manage users and user groups',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
usergroups: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Moderator': {
|
||||
name: 'Moderator',
|
||||
description: 'Can moderate content and manage activities',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: true, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: true, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: true, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Analyst': {
|
||||
name: 'Analyst',
|
||||
description: 'Read-only access with analytics capabilities',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Guest': {
|
||||
name: 'Guest',
|
||||
description: 'Limited access for external users',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: false, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function AddRole(props: AddRoleProps) {
|
||||
const org = useOrg() as any;
|
||||
const session = useLHSession() as any
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||
const [rights, setRights] = React.useState<Rights>(defaultRights)
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
org_id: org.id,
|
||||
rights: defaultRights
|
||||
},
|
||||
validate,
|
||||
onSubmit: async (values) => {
|
||||
const toastID = toast.loading("Creating...")
|
||||
setIsSubmitting(true)
|
||||
|
||||
// Ensure rights object is properly structured
|
||||
const formattedRights = {
|
||||
courses: {
|
||||
action_create: rights.courses?.action_create || false,
|
||||
action_read: rights.courses?.action_read || false,
|
||||
action_read_own: rights.courses?.action_read_own || false,
|
||||
action_update: rights.courses?.action_update || false,
|
||||
action_update_own: rights.courses?.action_update_own || false,
|
||||
action_delete: rights.courses?.action_delete || false,
|
||||
action_delete_own: rights.courses?.action_delete_own || false
|
||||
},
|
||||
users: {
|
||||
action_create: rights.users?.action_create || false,
|
||||
action_read: rights.users?.action_read || false,
|
||||
action_update: rights.users?.action_update || false,
|
||||
action_delete: rights.users?.action_delete || false
|
||||
},
|
||||
usergroups: {
|
||||
action_create: rights.usergroups?.action_create || false,
|
||||
action_read: rights.usergroups?.action_read || false,
|
||||
action_update: rights.usergroups?.action_update || false,
|
||||
action_delete: rights.usergroups?.action_delete || false
|
||||
},
|
||||
collections: {
|
||||
action_create: rights.collections?.action_create || false,
|
||||
action_read: rights.collections?.action_read || false,
|
||||
action_update: rights.collections?.action_update || false,
|
||||
action_delete: rights.collections?.action_delete || false
|
||||
},
|
||||
organizations: {
|
||||
action_create: rights.organizations?.action_create || false,
|
||||
action_read: rights.organizations?.action_read || false,
|
||||
action_update: rights.organizations?.action_update || false,
|
||||
action_delete: rights.organizations?.action_delete || false
|
||||
},
|
||||
coursechapters: {
|
||||
action_create: rights.coursechapters?.action_create || false,
|
||||
action_read: rights.coursechapters?.action_read || false,
|
||||
action_update: rights.coursechapters?.action_update || false,
|
||||
action_delete: rights.coursechapters?.action_delete || false
|
||||
},
|
||||
activities: {
|
||||
action_create: rights.activities?.action_create || false,
|
||||
action_read: rights.activities?.action_read || false,
|
||||
action_update: rights.activities?.action_update || false,
|
||||
action_delete: rights.activities?.action_delete || false
|
||||
},
|
||||
roles: {
|
||||
action_create: rights.roles?.action_create || false,
|
||||
action_read: rights.roles?.action_read || false,
|
||||
action_update: rights.roles?.action_update || false,
|
||||
action_delete: rights.roles?.action_delete || false
|
||||
},
|
||||
dashboard: {
|
||||
action_access: rights.dashboard?.action_access || false
|
||||
}
|
||||
}
|
||||
|
||||
const res = await createRole({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
org_id: values.org_id,
|
||||
rights: formattedRights
|
||||
}, access_token)
|
||||
if (res.status === 200 || res.status === 201) {
|
||||
setIsSubmitting(false)
|
||||
mutate(`${getAPIUrl()}roles/org/${org.id}`)
|
||||
props.setCreateRoleModal(false)
|
||||
toast.success("Created new role", {id:toastID})
|
||||
} else {
|
||||
setIsSubmitting(false)
|
||||
toast.error("Couldn't create new role", {id:toastID})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleRightChange = (section: keyof Rights, action: string, value: boolean) => {
|
||||
setRights(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[action]: value
|
||||
} as any
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSelectAll = (section: keyof Rights, value: boolean) => {
|
||||
setRights(prev => ({
|
||||
...prev,
|
||||
[section]: Object.keys(prev[section]).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: value
|
||||
}), {} as any)
|
||||
}))
|
||||
}
|
||||
|
||||
const handlePredefinedRole = (roleKey: string) => {
|
||||
const role = predefinedRoles[roleKey as keyof typeof predefinedRoles]
|
||||
if (role) {
|
||||
formik.setFieldValue('name', role.name)
|
||||
formik.setFieldValue('description', role.description)
|
||||
setRights(role.rights as Rights)
|
||||
}
|
||||
}
|
||||
|
||||
const PermissionSection = ({ title, icon: Icon, section, permissions }: { title: string, icon: any, section: keyof Rights, permissions: string[] }) => {
|
||||
const sectionRights = rights[section] as any
|
||||
const allSelected = permissions.every(perm => sectionRights[perm])
|
||||
const someSelected = permissions.some(perm => sectionRights[perm]) && !allSelected
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg p-4 mb-4 bg-white shadow-sm">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-3 gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="font-semibold text-gray-800 text-sm sm:text-base">{title}</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelectAll(section, !allSelected)}
|
||||
className="flex items-center space-x-2 text-sm text-blue-600 hover:text-blue-700 font-medium self-start sm:self-auto transition-colors"
|
||||
>
|
||||
{allSelected ? <CheckSquare className="w-4 h-4" /> : someSelected ? <Square className="w-4 h-4" /> : <Square className="w-4 h-4" />}
|
||||
<span className="hidden sm:inline">{allSelected ? 'Deselect All' : 'Select All'}</span>
|
||||
<span className="sm:hidden">{allSelected ? 'Deselect' : 'Select'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{permissions.map((permission) => (
|
||||
<label key={permission} className="flex items-center space-x-2 cursor-pointer p-2 rounded-md hover:bg-gray-50 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rights[section]?.[permission as keyof typeof rights[typeof section]] || false}
|
||||
onChange={(e) => handleRightChange(section, permission, e.target.checked)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 capitalize">
|
||||
{permission.replace('action_', '').replace('_', ' ')}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-3 max-w-6xl mx-auto px-2 sm:px-0">
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
<FormField name="name">
|
||||
<FormLabelAndMessage label="Role Name" message={formik.errors.name} />
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name}
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g., Course Manager"
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="description">
|
||||
<FormLabelAndMessage label="Description" message={formik.errors.description} />
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.description}
|
||||
required
|
||||
placeholder="Describe what this role can do..."
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">Predefined Rights</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{Object.keys(predefinedRoles).map((roleKey) => (
|
||||
<button
|
||||
key={roleKey}
|
||||
type="button"
|
||||
onClick={() => handlePredefinedRole(roleKey)}
|
||||
className="p-3 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-all duration-200 text-left bg-white shadow-sm hover:shadow-md"
|
||||
>
|
||||
<div className="font-medium text-gray-900 text-sm sm:text-base">{predefinedRoles[roleKey as keyof typeof predefinedRoles].name}</div>
|
||||
<div className="text-xs sm:text-sm text-gray-500 mt-1">{predefinedRoles[roleKey as keyof typeof predefinedRoles].description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">Permissions</h3>
|
||||
|
||||
<PermissionSection
|
||||
title="Courses"
|
||||
icon={BookOpen}
|
||||
section="courses"
|
||||
permissions={['action_create', 'action_read', 'action_read_own', 'action_update', 'action_update_own', 'action_delete', 'action_delete_own']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Users"
|
||||
icon={Users}
|
||||
section="users"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="User Groups"
|
||||
icon={UserCheck}
|
||||
section="usergroups"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Collections"
|
||||
icon={FolderOpen}
|
||||
section="collections"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Organizations"
|
||||
icon={Building}
|
||||
section="organizations"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Course Chapters"
|
||||
icon={FileText}
|
||||
section="coursechapters"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Activities"
|
||||
icon={Activity}
|
||||
section="activities"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Roles"
|
||||
icon={Shield}
|
||||
section="roles"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Dashboard"
|
||||
icon={Monitor}
|
||||
section="dashboard"
|
||||
permissions={['action_access']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 mt-6 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setCreateRoleModal(false)}
|
||||
className="px-4 py-2 text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors w-full sm:w-auto font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<Form.Submit asChild>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 w-full sm:w-auto font-medium shadow-sm"
|
||||
>
|
||||
{isSubmitting ? 'Creating...' : 'Create Role'}
|
||||
</button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddRole
|
||||
548
apps/web/components/Objects/Modals/Dash/OrgRoles/EditRole.tsx
Normal file
548
apps/web/components/Objects/Modals/Dash/OrgRoles/EditRole.tsx
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
'use client'
|
||||
import FormLayout, {
|
||||
FormField,
|
||||
FormLabelAndMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@components/Objects/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import React from 'react'
|
||||
import { updateRole } from '@services/roles/roles'
|
||||
import { mutate } from 'swr'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { useFormik } from 'formik'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Shield, BookOpen, Users, UserCheck, FolderOpen, Building, FileText, Activity, Settings, Monitor, CheckSquare, Square } from 'lucide-react'
|
||||
|
||||
type EditRoleProps = {
|
||||
role: {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
rights: any
|
||||
}
|
||||
setEditRoleModal: any
|
||||
}
|
||||
|
||||
interface Rights {
|
||||
courses: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_read_own: boolean;
|
||||
action_update: boolean;
|
||||
action_update_own: boolean;
|
||||
action_delete: boolean;
|
||||
action_delete_own: boolean;
|
||||
};
|
||||
users: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
usergroups: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
collections: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
organizations: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
coursechapters: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
activities: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
roles: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
dashboard: {
|
||||
action_access: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
|
||||
if (!values.name) {
|
||||
errors.name = 'Required'
|
||||
} else if (values.name.length < 2) {
|
||||
errors.name = 'Name must be at least 2 characters'
|
||||
}
|
||||
|
||||
if (!values.description) {
|
||||
errors.description = 'Required'
|
||||
} else if (values.description.length < 10) {
|
||||
errors.description = 'Description must be at least 10 characters'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const predefinedRoles = {
|
||||
'Admin': {
|
||||
name: 'Admin',
|
||||
description: 'Full platform control with all permissions',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: true, action_update_own: true, action_delete: true, action_delete_own: true },
|
||||
users: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
usergroups: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
collections: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
organizations: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
activities: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
roles: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Course Manager': {
|
||||
name: 'Course Manager',
|
||||
description: 'Can manage courses, chapters, and activities',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: true, action_update_own: true, action_delete: false, action_delete_own: true },
|
||||
users: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
collections: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
activities: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Instructor': {
|
||||
name: 'Instructor',
|
||||
description: 'Can create and manage their own courses',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: false, action_update_own: true, action_delete: false, action_delete_own: true },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: true, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Viewer': {
|
||||
name: 'Viewer',
|
||||
description: 'Read-only access to courses and content',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Content Creator': {
|
||||
name: 'Content Creator',
|
||||
description: 'Can create and edit content but not manage users',
|
||||
rights: {
|
||||
courses: { action_create: true, action_read: true, action_read_own: true, action_update: true, action_update_own: true, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
activities: { action_create: true, action_read: true, action_update: true, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'User Manager': {
|
||||
name: 'User Manager',
|
||||
description: 'Can manage users and user groups',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
usergroups: { action_create: true, action_read: true, action_update: true, action_delete: true },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Moderator': {
|
||||
name: 'Moderator',
|
||||
description: 'Can moderate content and manage activities',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: true, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: true, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: true, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Analyst': {
|
||||
name: 'Analyst',
|
||||
description: 'Read-only access with analytics capabilities',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: true, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: true }
|
||||
}
|
||||
},
|
||||
'Guest': {
|
||||
name: 'Guest',
|
||||
description: 'Limited access for external users',
|
||||
rights: {
|
||||
courses: { action_create: false, action_read: true, action_read_own: false, action_update: false, action_update_own: false, action_delete: false, action_delete_own: false },
|
||||
users: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
usergroups: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
collections: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
organizations: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
coursechapters: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
activities: { action_create: false, action_read: true, action_update: false, action_delete: false },
|
||||
roles: { action_create: false, action_read: false, action_update: false, action_delete: false },
|
||||
dashboard: { action_access: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function EditRole(props: EditRoleProps) {
|
||||
const org = useOrg() as any;
|
||||
const session = useLHSession() as any
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||
const [rights, setRights] = React.useState<Rights>(props.role.rights || {})
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: props.role.name,
|
||||
description: props.role.description,
|
||||
org_id: org.id,
|
||||
rights: props.role.rights || {}
|
||||
},
|
||||
validate,
|
||||
onSubmit: async (values) => {
|
||||
const toastID = toast.loading("Updating...")
|
||||
setIsSubmitting(true)
|
||||
|
||||
// Ensure rights object is properly structured
|
||||
const formattedRights = {
|
||||
courses: {
|
||||
action_create: rights.courses?.action_create || false,
|
||||
action_read: rights.courses?.action_read || false,
|
||||
action_read_own: rights.courses?.action_read_own || false,
|
||||
action_update: rights.courses?.action_update || false,
|
||||
action_update_own: rights.courses?.action_update_own || false,
|
||||
action_delete: rights.courses?.action_delete || false,
|
||||
action_delete_own: rights.courses?.action_delete_own || false
|
||||
},
|
||||
users: {
|
||||
action_create: rights.users?.action_create || false,
|
||||
action_read: rights.users?.action_read || false,
|
||||
action_update: rights.users?.action_update || false,
|
||||
action_delete: rights.users?.action_delete || false
|
||||
},
|
||||
usergroups: {
|
||||
action_create: rights.usergroups?.action_create || false,
|
||||
action_read: rights.usergroups?.action_read || false,
|
||||
action_update: rights.usergroups?.action_update || false,
|
||||
action_delete: rights.usergroups?.action_delete || false
|
||||
},
|
||||
collections: {
|
||||
action_create: rights.collections?.action_create || false,
|
||||
action_read: rights.collections?.action_read || false,
|
||||
action_update: rights.collections?.action_update || false,
|
||||
action_delete: rights.collections?.action_delete || false
|
||||
},
|
||||
organizations: {
|
||||
action_create: rights.organizations?.action_create || false,
|
||||
action_read: rights.organizations?.action_read || false,
|
||||
action_update: rights.organizations?.action_update || false,
|
||||
action_delete: rights.organizations?.action_delete || false
|
||||
},
|
||||
coursechapters: {
|
||||
action_create: rights.coursechapters?.action_create || false,
|
||||
action_read: rights.coursechapters?.action_read || false,
|
||||
action_update: rights.coursechapters?.action_update || false,
|
||||
action_delete: rights.coursechapters?.action_delete || false
|
||||
},
|
||||
activities: {
|
||||
action_create: rights.activities?.action_create || false,
|
||||
action_read: rights.activities?.action_read || false,
|
||||
action_update: rights.activities?.action_update || false,
|
||||
action_delete: rights.activities?.action_delete || false
|
||||
},
|
||||
roles: {
|
||||
action_create: rights.roles?.action_create || false,
|
||||
action_read: rights.roles?.action_read || false,
|
||||
action_update: rights.roles?.action_update || false,
|
||||
action_delete: rights.roles?.action_delete || false
|
||||
},
|
||||
dashboard: {
|
||||
action_access: rights.dashboard?.action_access || false
|
||||
}
|
||||
}
|
||||
|
||||
const res = await updateRole(props.role.id, {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
org_id: values.org_id,
|
||||
rights: formattedRights
|
||||
}, access_token)
|
||||
if (res.status === 200) {
|
||||
setIsSubmitting(false)
|
||||
mutate(`${getAPIUrl()}roles/org/${org.id}`)
|
||||
props.setEditRoleModal(false)
|
||||
toast.success("Updated role", {id:toastID})
|
||||
} else {
|
||||
setIsSubmitting(false)
|
||||
toast.error("Couldn't update role", {id:toastID})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleRightChange = (section: keyof Rights, action: string, value: boolean) => {
|
||||
setRights(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[action]: value
|
||||
} as any
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSelectAll = (section: keyof Rights, value: boolean) => {
|
||||
setRights(prev => ({
|
||||
...prev,
|
||||
[section]: Object.keys(prev[section]).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: value
|
||||
}), {} as any)
|
||||
}))
|
||||
}
|
||||
|
||||
const handlePredefinedRole = (roleKey: string) => {
|
||||
const role = predefinedRoles[roleKey as keyof typeof predefinedRoles]
|
||||
if (role) {
|
||||
formik.setFieldValue('name', role.name)
|
||||
formik.setFieldValue('description', role.description)
|
||||
setRights(role.rights as Rights)
|
||||
}
|
||||
}
|
||||
|
||||
const PermissionSection = ({ title, icon: Icon, section, permissions }: { title: string, icon: any, section: keyof Rights, permissions: string[] }) => {
|
||||
const sectionRights = rights[section] as any
|
||||
const allSelected = permissions.every(perm => sectionRights[perm])
|
||||
const someSelected = permissions.some(perm => sectionRights[perm]) && !allSelected
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg p-4 mb-4 bg-white shadow-sm">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-3 gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="font-semibold text-gray-800 text-sm sm:text-base">{title}</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelectAll(section, !allSelected)}
|
||||
className="flex items-center space-x-2 text-sm text-blue-600 hover:text-blue-700 font-medium self-start sm:self-auto transition-colors"
|
||||
>
|
||||
{allSelected ? <CheckSquare className="w-4 h-4" /> : someSelected ? <Square className="w-4 h-4" /> : <Square className="w-4 h-4" />}
|
||||
<span className="hidden sm:inline">{allSelected ? 'Deselect All' : 'Select All'}</span>
|
||||
<span className="sm:hidden">{allSelected ? 'Deselect' : 'Select'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{permissions.map((permission) => (
|
||||
<label key={permission} className="flex items-center space-x-2 cursor-pointer p-2 rounded-md hover:bg-gray-50 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rights[section]?.[permission as keyof typeof rights[typeof section]] || false}
|
||||
onChange={(e) => handleRightChange(section, permission, e.target.checked)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 capitalize">
|
||||
{permission.replace('action_', '').replace('_', ' ')}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-3 max-w-6xl mx-auto px-2 sm:px-0">
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
<FormField name="name">
|
||||
<FormLabelAndMessage label="Role Name" message={formik.errors.name} />
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name}
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g., Course Manager"
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="description">
|
||||
<FormLabelAndMessage label="Description" message={formik.errors.description} />
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.description}
|
||||
required
|
||||
placeholder="Describe what this role can do..."
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">Predefined Rights</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{Object.keys(predefinedRoles).map((roleKey) => (
|
||||
<button
|
||||
key={roleKey}
|
||||
type="button"
|
||||
onClick={() => handlePredefinedRole(roleKey)}
|
||||
className="p-3 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-all duration-200 text-left bg-white shadow-sm hover:shadow-md"
|
||||
>
|
||||
<div className="font-medium text-gray-900 text-sm sm:text-base">{predefinedRoles[roleKey as keyof typeof predefinedRoles].name}</div>
|
||||
<div className="text-xs sm:text-sm text-gray-500 mt-1">{predefinedRoles[roleKey as keyof typeof predefinedRoles].description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">Permissions</h3>
|
||||
|
||||
<PermissionSection
|
||||
title="Courses"
|
||||
icon={BookOpen}
|
||||
section="courses"
|
||||
permissions={['action_create', 'action_read', 'action_read_own', 'action_update', 'action_update_own', 'action_delete', 'action_delete_own']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Users"
|
||||
icon={Users}
|
||||
section="users"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="User Groups"
|
||||
icon={UserCheck}
|
||||
section="usergroups"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Collections"
|
||||
icon={FolderOpen}
|
||||
section="collections"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Organizations"
|
||||
icon={Building}
|
||||
section="organizations"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Course Chapters"
|
||||
icon={FileText}
|
||||
section="coursechapters"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Activities"
|
||||
icon={Activity}
|
||||
section="activities"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Roles"
|
||||
icon={Shield}
|
||||
section="roles"
|
||||
permissions={['action_create', 'action_read', 'action_update', 'action_delete']}
|
||||
/>
|
||||
|
||||
<PermissionSection
|
||||
title="Dashboard"
|
||||
icon={Monitor}
|
||||
section="dashboard"
|
||||
permissions={['action_access']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 mt-6 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setEditRoleModal(false)}
|
||||
className="px-4 py-2 text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors w-full sm:w-auto font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<Form.Submit asChild>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 w-full sm:w-auto font-medium shadow-sm"
|
||||
>
|
||||
{isSubmitting ? 'Updating...' : 'Update Role'}
|
||||
</button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditRole
|
||||
|
|
@ -11,10 +11,12 @@ 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 toast from 'react-hot-toast'
|
||||
import { BarLoader } from 'react-spinners'
|
||||
import { mutate } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
|
||||
interface Props {
|
||||
user: any
|
||||
|
|
@ -25,13 +27,19 @@ interface Props {
|
|||
function RolesUpdate(props: Props) {
|
||||
const org = useOrg() as any
|
||||
const session = useLHSession() as any
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||
const [assignedRole, setAssignedRole] = React.useState(
|
||||
props.alreadyAssignedRole
|
||||
)
|
||||
const [error, setError] = React.useState(null) as any
|
||||
|
||||
// Fetch available roles for the organization
|
||||
const { data: roles, error: rolesError } = useSWR(
|
||||
org ? `${getAPIUrl()}roles/org/${org.id}` : null,
|
||||
(url) => swrFetcher(url, access_token)
|
||||
)
|
||||
|
||||
const handleAssignedRole = (event: React.ChangeEvent<any>) => {
|
||||
setError(null)
|
||||
setAssignedRole(event.target.value)
|
||||
|
|
@ -80,10 +88,20 @@ function RolesUpdate(props: Props) {
|
|||
defaultValue={assignedRole}
|
||||
className="border border-gray-300 rounded-md p-2"
|
||||
required
|
||||
disabled={!roles || rolesError}
|
||||
>
|
||||
<option value="role_global_admin">Admin </option>
|
||||
<option value="role_global_maintainer">Maintainer</option>
|
||||
<option value="role_global_user">User</option>
|
||||
{!roles || rolesError ? (
|
||||
<option value="">Loading roles...</option>
|
||||
) : (
|
||||
<>
|
||||
<option value="">Select a role</option>
|
||||
{roles.map((role: any) => (
|
||||
<option key={role.id} value={role.role_uuid || role.id}>
|
||||
{role.name}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@ const Modal = (params: ModalParams) => {
|
|||
<DialogTrigger asChild>{params.dialogTrigger}</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className={cn(
|
||||
"overflow-auto",
|
||||
"w-[95vw] max-w-[95vw]",
|
||||
"max-h-[90vh]",
|
||||
"p-4",
|
||||
// Tablet and up
|
||||
"md:w-auto md:max-w-[90vw] md:p-6",
|
||||
"p-3 sm:p-4 md:p-6",
|
||||
// Mobile-first responsive design
|
||||
"sm:w-[90vw] sm:max-w-[90vw]",
|
||||
"md:w-auto md:max-w-[90vw]",
|
||||
"lg:max-w-[85vw]",
|
||||
"xl:max-w-[80vw]",
|
||||
getMinHeight(),
|
||||
getMinWidth(),
|
||||
params.customHeight,
|
||||
|
|
@ -60,15 +62,17 @@ const Modal = (params: ModalParams) => {
|
|||
)}>
|
||||
{params.dialogTitle && params.dialogDescription && (
|
||||
<DialogHeader className="text-center flex flex-col space-y-0.5 w-full">
|
||||
<DialogTitle>{params.dialogTitle}</DialogTitle>
|
||||
<DialogDescription>{params.dialogDescription}</DialogDescription>
|
||||
<DialogTitle className="text-lg sm:text-xl md:text-2xl">{params.dialogTitle}</DialogTitle>
|
||||
<DialogDescription className="text-sm sm:text-base">{params.dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
)}
|
||||
<div className="overflow-auto">
|
||||
{params.dialogContent}
|
||||
<div className="overflow-y-auto max-h-[calc(90vh-120px)] scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
|
||||
<div className="pr-2">
|
||||
{params.dialogContent}
|
||||
</div>
|
||||
</div>
|
||||
{(params.dialogClose || params.addDefCloseButton) && (
|
||||
<DialogFooter>
|
||||
<DialogFooter className="flex flex-col sm:flex-row gap-2 sm:gap-0">
|
||||
{params.dialogClose}
|
||||
{params.addDefCloseButton && (
|
||||
<ButtonBlack type="submit">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ interface RoleInfo {
|
|||
description: string;
|
||||
}
|
||||
|
||||
interface CustomRoleInfo {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const HeaderProfileBox = () => {
|
||||
const session = useLHSession() as any
|
||||
const { isAdmin, loading, userRoles, rights } = useAdminStatus()
|
||||
|
|
@ -103,6 +108,31 @@ export const HeaderProfileBox = () => {
|
|||
return roleConfigs[roleKey] || roleConfigs['role_global_user'];
|
||||
}, [userRoles, org?.id]);
|
||||
|
||||
const customRoles = useMemo((): CustomRoleInfo[] => {
|
||||
if (!userRoles || userRoles.length === 0) return [];
|
||||
|
||||
// Find roles for the current organization
|
||||
const orgRoles = userRoles.filter((role: any) => role.org.id === org?.id);
|
||||
|
||||
if (orgRoles.length === 0) return [];
|
||||
|
||||
// Filter for custom roles (not system roles)
|
||||
const customRoles = orgRoles.filter((role: any) => {
|
||||
// Check if it's a system role
|
||||
const isSystemRole =
|
||||
role.role.role_uuid?.startsWith('role_global_') ||
|
||||
[1, 2, 3, 4].includes(role.role.id) ||
|
||||
['Admin', 'Maintainer', 'Instructor', 'User'].includes(role.role.name);
|
||||
|
||||
return !isSystemRole;
|
||||
});
|
||||
|
||||
return customRoles.map((role: any) => ({
|
||||
name: role.role.name || 'Custom Role',
|
||||
description: role.role.description
|
||||
}));
|
||||
}, [userRoles, org?.id]);
|
||||
|
||||
return (
|
||||
<ProfileArea>
|
||||
{session.status == 'unauthenticated' && (
|
||||
|
|
@ -140,6 +170,20 @@ export const HeaderProfileBox = () => {
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Custom roles */}
|
||||
{customRoles.map((customRole, index) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
content={customRole.description || `Custom role: ${customRole.name}`}
|
||||
sideOffset={15}
|
||||
side="bottom"
|
||||
>
|
||||
<div className="text-[6px] bg-gray-500 text-white px-1 py-0.5 font-medium rounded-full flex items-center gap-0.5 w-fit">
|
||||
<Shield size={12} />
|
||||
{customRole.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<p className='text-xs text-gray-500'>{session.data.user.email}</p>
|
||||
</div>
|
||||
|
|
|
|||
68
apps/web/services/roles/roles.ts
Normal file
68
apps/web/services/roles/roles.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { getAPIUrl } from '@services/config/config'
|
||||
import { RequestBodyWithAuthHeader, getResponseMetadata } from '@services/utils/ts/requests'
|
||||
|
||||
/*
|
||||
Roles service matching available endpoints:
|
||||
- GET roles/org/{org_id}
|
||||
- POST roles/org/{org_id}
|
||||
- GET roles/{role_id}
|
||||
- PUT roles/{role_id}
|
||||
- DELETE roles/{role_id}
|
||||
|
||||
Note: GET requests are usually fetched with SWR directly from components.
|
||||
*/
|
||||
|
||||
export type CreateOrUpdateRoleBody = {
|
||||
name: string
|
||||
description?: string
|
||||
rights: any
|
||||
org_id?: number
|
||||
}
|
||||
|
||||
export async function createRole(body: CreateOrUpdateRoleBody, access_token: string) {
|
||||
const { org_id, ...payload } = body
|
||||
if (!org_id) throw new Error('createRole requires org_id in body')
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}roles/org/${org_id}`,
|
||||
RequestBodyWithAuthHeader('POST', payload, null, access_token)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
export async function getRole(role_id: number | string, access_token?: string) {
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}roles/${role_id}`,
|
||||
RequestBodyWithAuthHeader('GET', null, null, access_token)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
export async function updateRole(
|
||||
role_id: number | string,
|
||||
body: CreateOrUpdateRoleBody,
|
||||
access_token: string
|
||||
) {
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}roles/${role_id}`,
|
||||
RequestBodyWithAuthHeader('PUT', body, null, access_token)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
export async function deleteRole(
|
||||
role_id: number | string,
|
||||
_org_id: number | string | undefined,
|
||||
access_token: string
|
||||
) {
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}roles/${role_id}`,
|
||||
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue