feature/role-permission-system #6
@@ -5,7 +5,9 @@ COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
ARG VITE_WIZARD_PORT=18080
|
||||
ARG VITE_API_BASE
|
||||
ENV VITE_WIZARD_PORT=$VITE_WIZARD_PORT
|
||||
ENV VITE_API_BASE=$VITE_API_BASE
|
||||
RUN npm run build
|
||||
|
||||
# Production stage — lightweight static server, no nginx
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import api from '@/services/api'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
interface Permission {
|
||||
id: number
|
||||
@@ -11,18 +12,25 @@ interface Permission {
|
||||
interface Role {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
is_global: boolean
|
||||
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()
|
||||
@@ -68,6 +76,46 @@ export default function RoleEditorPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -83,6 +131,74 @@ export default function RoleEditorPage() {
|
||||
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',
|
||||
@@ -167,10 +283,27 @@ export default function RoleEditorPage() {
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn-primary"
|
||||
style={{ marginTop: '20px' }}
|
||||
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' }}>
|
||||
|
||||
Reference in New Issue
Block a user