Files
patchmon.net/frontend/src/pages/Permissions.jsx
Muhammad Ibrahim c5332ce6b0 first commit
2025-09-16 15:36:42 +01:00

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