mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-18 04:33:19 +00:00
Fixed permissions issues
Created default user role modified server.js to check if roles of admin/user is present modified server.js to check dashboard cards set up default dashboard cards to show
This commit is contained in:
@@ -8,7 +8,9 @@ const FirstTimeAdminSetup = () => {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
confirmPassword: '',
|
||||
firstName: '',
|
||||
lastName: ''
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -25,6 +27,14 @@ const FirstTimeAdminSetup = () => {
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (!formData.firstName.trim()) {
|
||||
setError('First name is required')
|
||||
return false
|
||||
}
|
||||
if (!formData.lastName.trim()) {
|
||||
setError('Last name is required')
|
||||
return false
|
||||
}
|
||||
if (!formData.username.trim()) {
|
||||
setError('Username is required')
|
||||
return false
|
||||
@@ -69,7 +79,9 @@ const FirstTimeAdminSetup = () => {
|
||||
body: JSON.stringify({
|
||||
username: formData.username.trim(),
|
||||
email: formData.email.trim(),
|
||||
password: formData.password
|
||||
password: formData.password,
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -145,6 +157,41 @@ const FirstTimeAdminSetup = () => {
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleInputChange}
|
||||
className="input w-full"
|
||||
placeholder="Enter your first name"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleInputChange}
|
||||
className="input w-full"
|
||||
placeholder="Enter your last name"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
Username
|
||||
|
||||
@@ -47,7 +47,7 @@ const Layout = ({ children }) => {
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||
const [githubStars, setGithubStars] = useState(null)
|
||||
const location = useLocation()
|
||||
const { user, logout, canViewHosts, canManageHosts, canViewPackages, canViewUsers, canManageUsers, canManageSettings } = useAuth()
|
||||
const { user, logout, canViewDashboard, canViewHosts, canManageHosts, canViewPackages, canViewUsers, canManageUsers, canViewReports, canExportData, canManageSettings } = useAuth()
|
||||
const { updateAvailable } = useUpdateNotification()
|
||||
const userMenuRef = useRef(null)
|
||||
|
||||
@@ -66,44 +66,103 @@ const Layout = ({ children }) => {
|
||||
staleTime: 300000, // Consider data stale after 5 minutes
|
||||
})
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: Home },
|
||||
{
|
||||
section: 'Inventory',
|
||||
items: [
|
||||
...(canViewHosts() ? [{ name: 'Hosts', href: '/hosts', icon: Server }] : []),
|
||||
...(canViewPackages() ? [{ name: 'Packages', href: '/packages', icon: Package }] : []),
|
||||
...(canViewHosts() ? [{ name: 'Repos', href: '/repositories', icon: GitBranch }] : []),
|
||||
{ name: 'Services', href: '/services', icon: Activity, comingSoon: true },
|
||||
{ name: 'Docker', href: '/docker', icon: Container, comingSoon: true },
|
||||
{ name: 'Reporting', href: '/reporting', icon: BarChart3, comingSoon: true },
|
||||
]
|
||||
},
|
||||
...(canViewUsers() || canManageUsers() ? [{
|
||||
section: 'PatchMon Users',
|
||||
items: [
|
||||
...(canViewUsers() ? [{ name: 'Users', href: '/users', icon: Users }] : []),
|
||||
...(canManageSettings() ? [{ name: 'Permissions', href: '/permissions', icon: Shield }] : []),
|
||||
]
|
||||
}] : []),
|
||||
{
|
||||
section: 'Settings',
|
||||
items: [
|
||||
...(canManageHosts() ? [{
|
||||
// Build navigation based on permissions
|
||||
const buildNavigation = () => {
|
||||
const nav = []
|
||||
|
||||
// Dashboard - only show if user can view dashboard
|
||||
if (canViewDashboard()) {
|
||||
nav.push({ name: 'Dashboard', href: '/', icon: Home })
|
||||
}
|
||||
|
||||
// Inventory section - only show if user has any inventory permissions
|
||||
if (canViewHosts() || canViewPackages() || canViewReports()) {
|
||||
const inventoryItems = []
|
||||
|
||||
if (canViewHosts()) {
|
||||
inventoryItems.push({ name: 'Hosts', href: '/hosts', icon: Server })
|
||||
inventoryItems.push({ name: 'Repos', href: '/repositories', icon: GitBranch })
|
||||
}
|
||||
|
||||
if (canViewPackages()) {
|
||||
inventoryItems.push({ name: 'Packages', href: '/packages', icon: Package })
|
||||
}
|
||||
|
||||
if (canViewReports()) {
|
||||
inventoryItems.push(
|
||||
{ name: 'Services', href: '/services', icon: Activity, comingSoon: true },
|
||||
{ name: 'Docker', href: '/docker', icon: Container, comingSoon: true },
|
||||
{ name: 'Reporting', href: '/reporting', icon: BarChart3, comingSoon: true }
|
||||
)
|
||||
}
|
||||
|
||||
if (inventoryItems.length > 0) {
|
||||
nav.push({
|
||||
section: 'Inventory',
|
||||
items: inventoryItems
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// PatchMon Users section - only show if user can view/manage users
|
||||
if (canViewUsers() || canManageUsers()) {
|
||||
const userItems = []
|
||||
|
||||
if (canViewUsers()) {
|
||||
userItems.push({ name: 'Users', href: '/users', icon: Users })
|
||||
}
|
||||
|
||||
if (canManageSettings()) {
|
||||
userItems.push({ name: 'Permissions', href: '/permissions', icon: Shield })
|
||||
}
|
||||
|
||||
if (userItems.length > 0) {
|
||||
nav.push({
|
||||
section: 'PatchMon Users',
|
||||
items: userItems
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Settings section - only show if user has any settings permissions
|
||||
if (canManageSettings() || canViewReports() || canExportData()) {
|
||||
const settingsItems = []
|
||||
|
||||
if (canManageSettings()) {
|
||||
settingsItems.push({
|
||||
name: 'PatchMon Options',
|
||||
href: '/options',
|
||||
icon: Settings
|
||||
}] : []),
|
||||
{ name: 'Audit Log', href: '/audit-log', icon: FileText, comingSoon: true },
|
||||
...(canManageSettings() ? [{
|
||||
})
|
||||
settingsItems.push({
|
||||
name: 'Server Config',
|
||||
href: '/settings',
|
||||
icon: Wrench,
|
||||
showUpgradeIcon: updateAvailable
|
||||
}] : []),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (canViewReports() || canExportData()) {
|
||||
settingsItems.push({
|
||||
name: 'Audit Log',
|
||||
href: '/audit-log',
|
||||
icon: FileText,
|
||||
comingSoon: true
|
||||
})
|
||||
}
|
||||
|
||||
if (settingsItems.length > 0) {
|
||||
nav.push({
|
||||
section: 'Settings',
|
||||
items: settingsItems
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return nav
|
||||
}
|
||||
|
||||
const navigation = buildNavigation()
|
||||
|
||||
const isActive = (path) => location.pathname === path
|
||||
|
||||
@@ -221,6 +280,15 @@ const Layout = ({ children }) => {
|
||||
</div>
|
||||
</div>
|
||||
<nav className="mt-8 flex-1 space-y-6 px-2">
|
||||
{/* Show message for users with very limited permissions */}
|
||||
{navigation.length === 0 && (
|
||||
<div className="px-2 py-4 text-center">
|
||||
<div className="text-sm text-secondary-500 dark:text-secondary-400">
|
||||
<p className="mb-2">Limited access</p>
|
||||
<p className="text-xs">Contact your administrator for additional permissions</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{navigation.map((item, index) => {
|
||||
if (item.name) {
|
||||
// Single item (Dashboard)
|
||||
@@ -346,6 +414,15 @@ const Layout = ({ children }) => {
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul className="flex flex-1 flex-col gap-y-6">
|
||||
{/* Show message for users with very limited permissions */}
|
||||
{navigation.length === 0 && (
|
||||
<li className="px-2 py-4 text-center">
|
||||
<div className="text-sm text-secondary-500 dark:text-secondary-400">
|
||||
<p className="mb-2">Limited access</p>
|
||||
<p className="text-xs">Contact your administrator for additional permissions</p>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{navigation.map((item, index) => {
|
||||
if (item.name) {
|
||||
// Single item (Dashboard)
|
||||
|
||||
@@ -9,7 +9,9 @@ const Login = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
password: '',
|
||||
firstName: '',
|
||||
lastName: ''
|
||||
})
|
||||
const [tfaData, setTfaData] = useState({
|
||||
token: ''
|
||||
@@ -76,8 +78,7 @@ const Login = () => {
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await authAPI.signup(formData.username, formData.email, formData.password)
|
||||
|
||||
const response = await authAPI.signup(formData.username, formData.email, formData.password, formData.firstName, formData.lastName)
|
||||
if (response.data && response.data.token) {
|
||||
// Store token and user data
|
||||
localStorage.setItem('token', response.data.token)
|
||||
@@ -162,7 +163,9 @@ const Login = () => {
|
||||
setFormData({
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
password: '',
|
||||
firstName: '',
|
||||
lastName: ''
|
||||
})
|
||||
setError('')
|
||||
}
|
||||
@@ -211,10 +214,53 @@ const Login = () => {
|
||||
</div>
|
||||
|
||||
{isSignupMode && (
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-secondary-700">
|
||||
Email
|
||||
</label>
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-secondary-700">
|
||||
First Name
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-secondary-400" />
|
||||
</div>
|
||||
<input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
required
|
||||
value={formData.firstName}
|
||||
onChange={handleInputChange}
|
||||
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Enter your first name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-secondary-700">
|
||||
Last Name
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-secondary-400" />
|
||||
</div>
|
||||
<input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
required
|
||||
value={formData.lastName}
|
||||
onChange={handleInputChange}
|
||||
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Enter your last name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-secondary-700">
|
||||
Email
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="email"
|
||||
@@ -235,6 +281,7 @@ const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
|
||||
@@ -174,7 +174,7 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
|
||||
onSave(role.role, permissions)
|
||||
}
|
||||
|
||||
const isAdminRole = role.role === 'admin'
|
||||
const isBuiltInRole = role.role === 'admin' || role.role === 'user'
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-secondary-800 shadow rounded-lg">
|
||||
@@ -183,9 +183,9 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
|
||||
<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 && (
|
||||
{isBuiltInRole && (
|
||||
<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
|
||||
Built-in Role
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -211,13 +211,13 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
|
||||
<>
|
||||
<button
|
||||
onClick={onEdit}
|
||||
disabled={isAdminRole}
|
||||
disabled={isBuiltInRole}
|
||||
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 && (
|
||||
{!isBuiltInRole && (
|
||||
<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"
|
||||
@@ -245,7 +245,7 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={(e) => handlePermissionChange(field.key, e.target.checked)}
|
||||
disabled={!isEditing || (isAdminRole && field.key === 'can_manage_users')}
|
||||
disabled={!isEditing || (isBuiltInRole && field.key === 'can_manage_users')}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Save, Server, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon, Clock } from 'lucide-react';
|
||||
import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api';
|
||||
import { settingsAPI, agentVersionAPI, versionAPI, permissionsAPI } from '../utils/api';
|
||||
import { useUpdateNotification } from '../contexts/UpdateNotificationContext';
|
||||
import UpgradeNotificationIcon from '../components/UpgradeNotificationIcon';
|
||||
|
||||
@@ -13,6 +13,7 @@ const Settings = () => {
|
||||
updateInterval: 60,
|
||||
autoUpdate: false,
|
||||
signupEnabled: false,
|
||||
defaultUserRole: 'user',
|
||||
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git',
|
||||
repositoryType: 'public',
|
||||
sshKeyPath: '',
|
||||
@@ -68,6 +69,12 @@ const Settings = () => {
|
||||
queryFn: () => settingsAPI.get().then(res => res.data)
|
||||
});
|
||||
|
||||
// Fetch available roles for default user role dropdown
|
||||
const { data: roles, isLoading: rolesLoading } = useQuery({
|
||||
queryKey: ['rolePermissions'],
|
||||
queryFn: () => permissionsAPI.getRoles().then(res => res.data)
|
||||
});
|
||||
|
||||
// Update form data when settings are loaded
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
@@ -78,6 +85,7 @@ const Settings = () => {
|
||||
updateInterval: settings.update_interval || 60,
|
||||
autoUpdate: settings.auto_update || false,
|
||||
signupEnabled: settings.signup_enabled === true ? true : false, // Explicit boolean conversion
|
||||
defaultUserRole: settings.default_user_role || 'user',
|
||||
githubRepoUrl: settings.github_repo_url || 'git@github.com:9technologygroup/patchmon.net.git',
|
||||
repositoryType: settings.repository_type || 'public',
|
||||
sshKeyPath: settings.ssh_key_path || '',
|
||||
@@ -560,6 +568,37 @@ const Settings = () => {
|
||||
Enable User Self-Registration
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Default User Role Dropdown */}
|
||||
{formData.signupEnabled && (
|
||||
<div className="mt-3 ml-6">
|
||||
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
|
||||
Default Role for New Users
|
||||
</label>
|
||||
<select
|
||||
value={formData.defaultUserRole}
|
||||
onChange={(e) => handleInputChange('defaultUserRole', e.target.value)}
|
||||
className="w-full max-w-xs border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
||||
disabled={rolesLoading}
|
||||
>
|
||||
{rolesLoading ? (
|
||||
<option>Loading roles...</option>
|
||||
) : roles && Array.isArray(roles) ? (
|
||||
roles.map((role) => (
|
||||
<option key={role.role} value={role.role}>
|
||||
{role.role.charAt(0).toUpperCase() + role.role.slice(1)}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="user">User</option>
|
||||
)}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||
New users will be assigned this role when they register.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
When enabled, users can create their own accounts through the signup page. When disabled, only administrators can create user accounts.
|
||||
</p>
|
||||
|
||||
@@ -201,7 +201,7 @@ export const versionAPI = {
|
||||
export const authAPI = {
|
||||
login: (username, password) => api.post('/auth/login', { username, password }),
|
||||
verifyTfa: (username, token) => api.post('/auth/verify-tfa', { username, token }),
|
||||
signup: (username, email, password) => api.post('/auth/signup', { username, email, password }),
|
||||
signup: (username, email, password, firstName, lastName) => api.post('/auth/signup', { username, email, password, firstName, lastName }),
|
||||
}
|
||||
|
||||
// TFA API
|
||||
|
||||
Reference in New Issue
Block a user