feat: enhance role management API with organization-specific role creation and retrieval, including comprehensive RBAC checks for permissions

This commit is contained in:
swve 2025-08-09 14:26:48 +02:00
parent 3ce019abec
commit 531e1863c0
10 changed files with 2174 additions and 32 deletions

View file

@ -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>
)

View 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

View 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

View 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

View file

@ -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>

View file

@ -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">

View file

@ -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>

View 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
}