mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-24 08:33:38 +00:00
390 lines
14 KiB
JavaScript
390 lines
14 KiB
JavaScript
import React, { useState } from 'react'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import {
|
|
Shield,
|
|
Settings,
|
|
Users,
|
|
Server,
|
|
Package,
|
|
BarChart3,
|
|
Download,
|
|
Eye,
|
|
Edit,
|
|
Trash2,
|
|
Plus,
|
|
Save,
|
|
X,
|
|
AlertTriangle,
|
|
RefreshCw
|
|
} from 'lucide-react'
|
|
import { permissionsAPI } from '../utils/api'
|
|
import { useAuth } from '../contexts/AuthContext'
|
|
|
|
const Permissions = () => {
|
|
const [editingRole, setEditingRole] = useState(null)
|
|
const [showAddModal, setShowAddModal] = useState(false)
|
|
const queryClient = useQueryClient()
|
|
const { refreshPermissions } = useAuth()
|
|
|
|
// Fetch all role permissions
|
|
const { data: roles, isLoading, error } = useQuery({
|
|
queryKey: ['rolePermissions'],
|
|
queryFn: () => permissionsAPI.getRoles().then(res => res.data)
|
|
})
|
|
|
|
// Update role permissions mutation
|
|
const updateRoleMutation = useMutation({
|
|
mutationFn: ({ role, permissions }) => permissionsAPI.updateRole(role, permissions),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries(['rolePermissions'])
|
|
setEditingRole(null)
|
|
// Refresh user permissions to apply changes immediately
|
|
refreshPermissions()
|
|
}
|
|
})
|
|
|
|
// Delete role mutation
|
|
const deleteRoleMutation = useMutation({
|
|
mutationFn: (role) => permissionsAPI.deleteRole(role),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries(['rolePermissions'])
|
|
}
|
|
})
|
|
|
|
const handleSavePermissions = async (role, permissions) => {
|
|
try {
|
|
await updateRoleMutation.mutateAsync({ role, permissions })
|
|
} catch (error) {
|
|
console.error('Failed to update permissions:', error)
|
|
}
|
|
}
|
|
|
|
const handleDeleteRole = async (role) => {
|
|
if (window.confirm(`Are you sure you want to delete the "${role}" role? This action cannot be undone.`)) {
|
|
try {
|
|
await deleteRoleMutation.mutateAsync(role)
|
|
} catch (error) {
|
|
console.error('Failed to delete role:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-danger-50 border border-danger-200 rounded-md p-4">
|
|
<div className="flex">
|
|
<AlertTriangle className="h-5 w-5 text-danger-400" />
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-danger-800">Error loading permissions</h3>
|
|
<p className="mt-1 text-sm text-danger-700">{error.message}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex justify-end items-center">
|
|
<div className="flex space-x-3">
|
|
<button
|
|
onClick={() => refreshPermissions()}
|
|
className="inline-flex items-center px-4 py-2 border border-secondary-300 text-sm font-medium rounded-md text-secondary-700 bg-white hover:bg-secondary-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
|
>
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Refresh Permissions
|
|
</button>
|
|
<button
|
|
onClick={() => setShowAddModal(true)}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Role
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Roles List */}
|
|
<div className="space-y-4">
|
|
{roles && Array.isArray(roles) && roles.map((role) => (
|
|
<RolePermissionsCard
|
|
key={role.id}
|
|
role={role}
|
|
isEditing={editingRole === role.role}
|
|
onEdit={() => setEditingRole(role.role)}
|
|
onCancel={() => setEditingRole(null)}
|
|
onSave={handleSavePermissions}
|
|
onDelete={handleDeleteRole}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Add Role Modal */}
|
|
<AddRoleModal
|
|
isOpen={showAddModal}
|
|
onClose={() => setShowAddModal(false)}
|
|
onSuccess={() => {
|
|
queryClient.invalidateQueries(['rolePermissions'])
|
|
setShowAddModal(false)
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Role Permissions Card Component
|
|
const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDelete }) => {
|
|
const [permissions, setPermissions] = useState(role)
|
|
|
|
const permissionFields = [
|
|
{ key: 'canViewDashboard', label: 'View Dashboard', icon: BarChart3, description: 'Access to the main dashboard' },
|
|
{ key: 'canViewHosts', label: 'View Hosts', icon: Server, description: 'See host information and status' },
|
|
{ key: 'canManageHosts', label: 'Manage Hosts', icon: Edit, description: 'Add, edit, and delete hosts' },
|
|
{ key: 'canViewPackages', label: 'View Packages', icon: Package, description: 'See package information' },
|
|
{ key: 'canManagePackages', label: 'Manage Packages', icon: Settings, description: 'Edit package details' },
|
|
{ key: 'canViewUsers', label: 'View Users', icon: Users, description: 'See user list and details' },
|
|
{ key: 'canManageUsers', label: 'Manage Users', icon: Shield, description: 'Add, edit, and delete users' },
|
|
{ key: 'canViewReports', label: 'View Reports', icon: BarChart3, description: 'Access to reports and analytics' },
|
|
{ key: 'canExportData', label: 'Export Data', icon: Download, description: 'Download data and reports' },
|
|
{ key: 'canManageSettings', label: 'Manage Settings', icon: Settings, description: 'System configuration access' }
|
|
]
|
|
|
|
const handlePermissionChange = (key, value) => {
|
|
setPermissions(prev => ({
|
|
...prev,
|
|
[key]: value
|
|
}))
|
|
}
|
|
|
|
const handleSave = () => {
|
|
onSave(role.role, permissions)
|
|
}
|
|
|
|
const isAdminRole = role.role === 'admin'
|
|
|
|
return (
|
|
<div className="bg-white dark:bg-secondary-800 shadow rounded-lg">
|
|
<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-600">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<Shield className="h-5 w-5 text-primary-600 mr-3" />
|
|
<h3 className="text-lg font-medium text-secondary-900 dark:text-white capitalize">{role.role}</h3>
|
|
{isAdminRole && (
|
|
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
|
|
System Role
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{isEditing ? (
|
|
<>
|
|
<button
|
|
onClick={handleSave}
|
|
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700"
|
|
>
|
|
<Save className="h-4 w-4 mr-1" />
|
|
Save
|
|
</button>
|
|
<button
|
|
onClick={onCancel}
|
|
className="inline-flex items-center px-3 py-1 border border-secondary-300 text-sm font-medium rounded-md text-secondary-700 bg-white hover:bg-secondary-50"
|
|
>
|
|
<X className="h-4 w-4 mr-1" />
|
|
Cancel
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<button
|
|
onClick={onEdit}
|
|
disabled={isAdminRole}
|
|
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Edit className="h-4 w-4 mr-1" />
|
|
Edit
|
|
</button>
|
|
{!isAdminRole && (
|
|
<button
|
|
onClick={() => onDelete(role.role)}
|
|
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-1" />
|
|
Delete
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-6 py-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{permissionFields.map((field) => {
|
|
const Icon = field.icon
|
|
const isChecked = permissions[field.key]
|
|
|
|
return (
|
|
<div key={field.key} className="flex items-start">
|
|
<div className="flex items-center h-5">
|
|
<input
|
|
type="checkbox"
|
|
checked={isChecked}
|
|
onChange={(e) => handlePermissionChange(field.key, e.target.checked)}
|
|
disabled={!isEditing || (isAdminRole && field.key === 'canManageUsers')}
|
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
<div className="ml-3">
|
|
<div className="flex items-center">
|
|
<Icon className="h-4 w-4 text-secondary-400 mr-2" />
|
|
<label className="text-sm font-medium text-secondary-900 dark:text-white">
|
|
{field.label}
|
|
</label>
|
|
</div>
|
|
<p className="text-xs text-secondary-500 mt-1">
|
|
{field.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Add Role Modal Component
|
|
const AddRoleModal = ({ isOpen, onClose, onSuccess }) => {
|
|
const [formData, setFormData] = useState({
|
|
role: '',
|
|
canViewDashboard: true,
|
|
canViewHosts: true,
|
|
canManageHosts: false,
|
|
canViewPackages: true,
|
|
canManagePackages: false,
|
|
canViewUsers: false,
|
|
canManageUsers: false,
|
|
canViewReports: true,
|
|
canExportData: false,
|
|
canManageSettings: false
|
|
})
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault()
|
|
setIsLoading(true)
|
|
setError('')
|
|
|
|
try {
|
|
await permissionsAPI.updateRole(formData.role, formData)
|
|
onSuccess()
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to create role')
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleInputChange = (e) => {
|
|
const { name, value, type, checked } = e.target
|
|
setFormData({
|
|
...formData,
|
|
[name]: type === 'checkbox' ? checked : value
|
|
})
|
|
}
|
|
|
|
if (!isOpen) return null
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<h3 className="text-lg font-medium text-secondary-900 mb-4">Add New Role</h3>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary-700 mb-1">
|
|
Role Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="role"
|
|
required
|
|
value={formData.role}
|
|
onChange={handleInputChange}
|
|
className="block w-full border-secondary-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
|
placeholder="e.g., host_manager, readonly"
|
|
/>
|
|
<p className="mt-1 text-xs text-secondary-500">Use lowercase with underscores (e.g., host_manager)</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium text-secondary-900 dark:text-white">Permissions</h4>
|
|
{[
|
|
{ key: 'canViewDashboard', label: 'View Dashboard' },
|
|
{ key: 'canViewHosts', label: 'View Hosts' },
|
|
{ key: 'canManageHosts', label: 'Manage Hosts' },
|
|
{ key: 'canViewPackages', label: 'View Packages' },
|
|
{ key: 'canManagePackages', label: 'Manage Packages' },
|
|
{ key: 'canViewUsers', label: 'View Users' },
|
|
{ key: 'canManageUsers', label: 'Manage Users' },
|
|
{ key: 'canViewReports', label: 'View Reports' },
|
|
{ key: 'canExportData', label: 'Export Data' },
|
|
{ key: 'canManageSettings', label: 'Manage Settings' }
|
|
].map((permission) => (
|
|
<div key={permission.key} className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
name={permission.key}
|
|
checked={formData[permission.key]}
|
|
onChange={handleInputChange}
|
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
|
|
/>
|
|
<label className="ml-2 block text-sm text-secondary-700">
|
|
{permission.label}
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-danger-50 border border-danger-200 rounded-md p-3">
|
|
<p className="text-sm text-danger-700">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end space-x-3">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm font-medium text-secondary-700 bg-white border border-secondary-300 rounded-md hover:bg-secondary-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 border border-transparent rounded-md hover:bg-primary-700 disabled:opacity-50"
|
|
>
|
|
{isLoading ? 'Creating...' : 'Create Role'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Permissions
|