- Add RoleEditorPage with role management - Add Create New Role button (admin only) - Add Delete Role button (admin only, admin role protected) - Fix useAuth import in RoleEditorPage
318 lines
11 KiB
TypeScript
318 lines
11 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import api from '@/services/api'
|
|
import { useAuth } from '@/hooks/useAuth'
|
|
|
|
interface Permission {
|
|
id: number
|
|
name: string
|
|
description: string
|
|
category: string
|
|
}
|
|
|
|
interface Role {
|
|
id: number
|
|
name: string
|
|
description: string | null
|
|
is_global: boolean | null
|
|
permission_ids: number[]
|
|
}
|
|
|
|
export default function RoleEditorPage() {
|
|
const { user } = useAuth()
|
|
const [roles, setRoles] = useState<Role[]>([])
|
|
const [permissions, setPermissions] = useState<Permission[]>([])
|
|
const [selectedRole, setSelectedRole] = useState<Role | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
const [message, setMessage] = useState('')
|
|
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
const [newRoleName, setNewRoleName] = useState('')
|
|
const [newRoleDesc, setNewRoleDesc] = useState('')
|
|
const [creating, setCreating] = useState(false)
|
|
|
|
const isAdmin = user?.is_admin === true
|
|
|
|
useEffect(() => {
|
|
fetchData()
|
|
}, [])
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
const [rolesRes, permsRes] = await Promise.all([
|
|
api.get('/roles'),
|
|
api.get('/roles/permissions')
|
|
])
|
|
setRoles(rolesRes.data)
|
|
setPermissions(permsRes.data)
|
|
} catch (err) {
|
|
console.error('Failed to fetch data:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handlePermissionToggle = (permId: number) => {
|
|
if (!selectedRole) return
|
|
const newPermIds = selectedRole.permission_ids.includes(permId)
|
|
? selectedRole.permission_ids.filter(id => id !== permId)
|
|
: [...selectedRole.permission_ids, permId]
|
|
setSelectedRole({ ...selectedRole, permission_ids: newPermIds })
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
if (!selectedRole) return
|
|
setSaving(true)
|
|
setMessage('')
|
|
try {
|
|
await api.post(`/roles/${selectedRole.id}/permissions`, {
|
|
permission_ids: selectedRole.permission_ids
|
|
})
|
|
setMessage('Saved successfully!')
|
|
fetchData()
|
|
} catch (err: any) {
|
|
setMessage(err.response?.data?.detail || 'Failed to save')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleDeleteRole = async () => {
|
|
if (!selectedRole || !confirm(`Are you sure you want to delete the "${selectedRole.name}" role?`)) return
|
|
setSaving(true)
|
|
setMessage('')
|
|
try {
|
|
await api.delete(`/roles/${selectedRole.id}`)
|
|
setMessage('Role deleted successfully!')
|
|
setSelectedRole(null)
|
|
fetchData()
|
|
} catch (err: any) {
|
|
setMessage(err.response?.data?.detail || 'Failed to delete role')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const canDeleteRole = selectedRole && selectedRole.name !== 'admin' && isAdmin
|
|
|
|
const handleCreateRole = async () => {
|
|
if (!newRoleName.trim()) return
|
|
setCreating(true)
|
|
setMessage('')
|
|
try {
|
|
await api.post('/roles', {
|
|
name: newRoleName.trim(),
|
|
description: newRoleDesc.trim() || null,
|
|
is_global: false
|
|
})
|
|
setMessage('Role created successfully!')
|
|
setShowCreateForm(false)
|
|
setNewRoleName('')
|
|
setNewRoleDesc('')
|
|
fetchData()
|
|
} catch (err: any) {
|
|
setMessage(err.response?.data?.detail || 'Failed to create role')
|
|
} finally {
|
|
setCreating(false)
|
|
}
|
|
}
|
|
|
|
const groupedPermissions = permissions.reduce((acc, p) => {
|
|
if (!acc[p.category]) acc[p.category] = []
|
|
acc[p.category].push(p)
|
|
return acc
|
|
}, {} as Record<string, Permission[]>)
|
|
|
|
if (loading) return <div className="p-4">Loading...</div>
|
|
|
|
return (
|
|
<div className="role-editor-page" style={{ padding: '20px' }}>
|
|
<h2>🔐 Role Editor</h2>
|
|
<p style={{ color: '#888', marginBottom: '20px' }}>
|
|
Configure permissions for each role. Only admins can edit roles.
|
|
</p>
|
|
|
|
{isAdmin && !showCreateForm && (
|
|
<button
|
|
onClick={() => setShowCreateForm(true)}
|
|
className="btn-primary"
|
|
style={{ marginBottom: '20px' }}
|
|
>
|
|
+ Create New Role
|
|
</button>
|
|
)}
|
|
|
|
{showCreateForm && (
|
|
<div style={{
|
|
border: '2px solid #007bff',
|
|
borderRadius: '8px',
|
|
padding: '20px',
|
|
marginBottom: '20px',
|
|
backgroundColor: '#f8f9fa'
|
|
}}>
|
|
<h3 style={{ marginTop: 0 }}>Create New Role</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: '400px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 500 }}>
|
|
Role Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newRoleName}
|
|
onChange={(e) => setNewRoleName(e.target.value)}
|
|
placeholder="e.g., developer, manager"
|
|
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 500 }}>
|
|
Description
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newRoleDesc}
|
|
onChange={(e) => setNewRoleDesc(e.target.value)}
|
|
placeholder="Role description"
|
|
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
/>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<button
|
|
onClick={handleCreateRole}
|
|
disabled={creating || !newRoleName.trim()}
|
|
className="btn-primary"
|
|
>
|
|
{creating ? 'Creating...' : 'Create'}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setShowCreateForm(false)
|
|
setNewRoleName('')
|
|
setNewRoleDesc('')
|
|
}}
|
|
className="btn-secondary"
|
|
style={{ padding: '8px 16px', borderRadius: '4px', border: '1px solid #ddd', background: '#fff' }}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{message && (
|
|
<div style={{
|
|
padding: '10px',
|
|
marginBottom: '20px',
|
|
backgroundColor: message.includes('success') ? '#d4edda' : '#f8d7da',
|
|
borderRadius: '4px'
|
|
}}>
|
|
{message}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', gap: '20px' }}>
|
|
{/* Role List */}
|
|
<div style={{ width: '250px' }}>
|
|
<h3>Roles</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
{roles.map(role => (
|
|
<div
|
|
key={role.id}
|
|
onClick={() => setSelectedRole({ ...role })}
|
|
style={{
|
|
padding: '12px',
|
|
border: selectedRole?.id === role.id ? '2px solid #007bff' : '1px solid #ddd',
|
|
borderRadius: '6px',
|
|
cursor: 'pointer',
|
|
backgroundColor: selectedRole?.id === role.id ? '#f0f8ff' : 'white'
|
|
}}
|
|
>
|
|
<strong>{role.name}</strong>
|
|
{role.is_global && <span style={{ fontSize: '12px', marginLeft: '8px', color: '#ff6b6b' }}>🌟</span>}
|
|
<div style={{ fontSize: '12px', color: '#666' }}>{role.description}</div>
|
|
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
|
|
{role.permission_ids.length} permissions
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Permission Editor */}
|
|
<div style={{ flex: 1 }}>
|
|
{selectedRole ? (
|
|
<>
|
|
<h3>Permissions for: {selectedRole.name}</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
|
<div key={category} style={{
|
|
border: '1px solid #eee',
|
|
borderRadius: '8px',
|
|
padding: '12px'
|
|
}}>
|
|
<h4 style={{ margin: '0 0 10px 0', textTransform: 'capitalize' }}>
|
|
{category}
|
|
</h4>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '8px' }}>
|
|
{perms.map(perm => (
|
|
<label key={perm.id} style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
cursor: 'pointer',
|
|
padding: '6px',
|
|
borderRadius: '4px',
|
|
backgroundColor: selectedRole.permission_ids.includes(perm.id) ? '#e8f5e9' : 'transparent'
|
|
}}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedRole.permission_ids.includes(perm.id)}
|
|
onChange={() => handlePermissionToggle(perm.id)}
|
|
/>
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{perm.name}</div>
|
|
<div style={{ fontSize: '11px', color: '#666' }}>{perm.description}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="btn-primary"
|
|
style={{ marginTop: '20px', marginRight: '10px' }}
|
|
>
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
{canDeleteRole && (
|
|
<button
|
|
onClick={handleDeleteRole}
|
|
disabled={saving}
|
|
style={{
|
|
marginTop: '20px',
|
|
padding: '10px 20px',
|
|
borderRadius: '4px',
|
|
border: '1px solid #dc3545',
|
|
backgroundColor: '#dc3545',
|
|
color: 'white',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
Delete Role
|
|
</button>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div style={{ color: '#888', textAlign: 'center', marginTop: '40px' }}>
|
|
Select a role to edit its permissions
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|