feature/role-permission-system #6

Merged
hzhang merged 3 commits from feature/role-permission-system into main 2026-03-15 15:44:56 +00:00
2 changed files with 138 additions and 3 deletions
Showing only changes of commit 800a618aaa - Show all commits

View File

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

View File

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