From ed0cf79b53a0520c338eab137f31dca20ef67021 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Tue, 30 Sep 2025 19:48:28 +0100 Subject: [PATCH] Added settings pages to bring all the settings together from patchmon options, profile page and server settings. --- backend/src/routes/hostGroupRoutes.js | 8 +- frontend/src/App.jsx | 156 ++- frontend/src/components/Layout.jsx | 236 +++-- frontend/src/components/SettingsLayout.jsx | 248 +++++ .../settings/AgentManagementTab.jsx | 368 +++++++ .../components/settings/AgentUpdatesTab.jsx | 425 +++++++++ .../components/settings/ProtocolUrlTab.jsx | 305 ++++++ frontend/src/components/settings/RolesTab.jsx | 570 +++++++++++ frontend/src/components/settings/UsersTab.jsx | 897 ++++++++++++++++++ .../components/settings/VersionUpdateTab.jsx | 573 +++++++++++ frontend/src/pages/Hosts.jsx | 7 + frontend/src/pages/Profile.jsx | 103 +- .../pages/settings/SettingsAgentConfig.jsx | 89 ++ .../src/pages/settings/SettingsHostGroups.jsx | 599 ++++++++++++ .../pages/settings/SettingsServerConfig.jsx | 96 ++ frontend/src/pages/settings/SettingsUsers.jsx | 107 +++ 16 files changed, 4638 insertions(+), 149 deletions(-) create mode 100644 frontend/src/components/SettingsLayout.jsx create mode 100644 frontend/src/components/settings/AgentManagementTab.jsx create mode 100644 frontend/src/components/settings/AgentUpdatesTab.jsx create mode 100644 frontend/src/components/settings/ProtocolUrlTab.jsx create mode 100644 frontend/src/components/settings/RolesTab.jsx create mode 100644 frontend/src/components/settings/UsersTab.jsx create mode 100644 frontend/src/components/settings/VersionUpdateTab.jsx create mode 100644 frontend/src/pages/settings/SettingsAgentConfig.jsx create mode 100644 frontend/src/pages/settings/SettingsHostGroups.jsx create mode 100644 frontend/src/pages/settings/SettingsServerConfig.jsx create mode 100644 frontend/src/pages/settings/SettingsUsers.jsx diff --git a/backend/src/routes/hostGroupRoutes.js b/backend/src/routes/hostGroupRoutes.js index cc9b491..42300be 100644 --- a/backend/src/routes/hostGroupRoutes.js +++ b/backend/src/routes/hostGroupRoutes.js @@ -205,11 +205,11 @@ router.delete( return res.status(404).json({ error: "Host group not found" }); } - // Check if host group has hosts + // If host group has hosts, ungroup them first if (existingGroup._count.hosts > 0) { - return res.status(400).json({ - error: - "Cannot delete host group that contains hosts. Please move or remove hosts first.", + await prisma.hosts.updateMany({ + where: { host_group_id: id }, + data: { host_group_id: null }, }); } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e6a3efd..3ef31c7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,6 +2,7 @@ import { Route, Routes } from "react-router-dom"; import FirstTimeAdminSetup from "./components/FirstTimeAdminSetup"; import Layout from "./components/Layout"; import ProtectedRoute from "./components/ProtectedRoute"; +import SettingsLayout from "./components/SettingsLayout"; import { isAuthPhase } from "./constants/authPhases"; import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { ThemeProvider } from "./contexts/ThemeContext"; @@ -10,15 +11,16 @@ import Dashboard from "./pages/Dashboard"; import HostDetail from "./pages/HostDetail"; import Hosts from "./pages/Hosts"; import Login from "./pages/Login"; -import Options from "./pages/Options"; import PackageDetail from "./pages/PackageDetail"; import Packages from "./pages/Packages"; -import Permissions from "./pages/Permissions"; import Profile from "./pages/Profile"; import Repositories from "./pages/Repositories"; import RepositoryDetail from "./pages/RepositoryDetail"; import Settings from "./pages/Settings"; -import Users from "./pages/Users"; +import SettingsAgentConfig from "./pages/settings/SettingsAgentConfig"; +import SettingsHostGroups from "./pages/settings/SettingsHostGroups"; +import SettingsServerConfig from "./pages/settings/SettingsServerConfig"; +import SettingsUsers from "./pages/settings/SettingsUsers"; function AppRoutes() { const { needsFirstTimeSetup, authPhase, isAuthenticated } = useAuth(); @@ -114,7 +116,7 @@ function AppRoutes() { element={ - + } @@ -124,13 +126,115 @@ function AppRoutes() { element={ - + } /> + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + @@ -139,22 +243,42 @@ function AppRoutes() { } /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> - - - - } - /> - - - + } diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index d3c735d..3c0de48 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -141,69 +141,41 @@ const Layout = ({ children }) => { } } - // 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, - }); - 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; }; + // Build settings navigation separately (for bottom placement) + const buildSettingsNavigation = () => { + const settingsNav = []; + + // Settings section - consolidated all settings into one page + if ( + canManageSettings() || + canViewUsers() || + canManageUsers() || + canViewReports() || + canExportData() + ) { + const settingsItems = []; + + settingsItems.push({ + name: "Settings", + href: "/settings/users", + icon: Settings, + showUpgradeIcon: updateAvailable, + }); + + settingsNav.push({ + section: "Settings", + items: settingsItems, + }); + } + + return settingsNav; + }; + const navigation = buildNavigation(); + const settingsNavigation = buildSettingsNavigation(); const isActive = (path) => location.pathname === path; @@ -223,9 +195,10 @@ const Layout = ({ children }) => { if (path === "/settings") return "Settings"; if (path === "/options") return "PatchMon Options"; if (path === "/audit-log") return "Audit Log"; - if (path === "/profile") return "My Profile"; + if (path === "/settings/profile") return "My Profile"; if (path.startsWith("/hosts/")) return "Host Details"; if (path.startsWith("/packages/")) return "Package Details"; + if (path.startsWith("/settings/")) return "Settings"; return "PatchMon"; }; @@ -342,7 +315,7 @@ const Layout = ({ children }) => { @@ -493,7 +512,7 @@ const Layout = ({ children }) => { {/* Profile Section - Bottom of Sidebar */} @@ -637,11 +723,11 @@ const Layout = ({ children }) => { {!sidebarCollapsed ? (
{/* User Info with Sign Out - Username is clickable */} -
+
{
-
+
{ {user?.first_name || user?.username} {user?.role === "admin" && ( - - Admin + + Role: Admin )}
@@ -712,9 +804,9 @@ const Layout = ({ children }) => { ) : (
{ + const location = useLocation(); + const { canManageSettings, canViewUsers, canManageUsers } = useAuth(); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + + // Build secondary navigation based on permissions + const buildSecondaryNavigation = () => { + const nav = []; + + // Users section + if (canViewUsers() || canManageUsers()) { + nav.push({ + section: "User Management", + items: [ + { + name: "Users", + href: "/settings/users", + icon: Users, + }, + { + name: "Roles", + href: "/settings/roles", + icon: Shield, + }, + { + name: "My Profile", + href: "/settings/profile", + icon: UserCircle, + }, + ], + }); + } + + // Host Groups + if (canManageSettings()) { + nav.push({ + section: "Hosts Management", + items: [ + { + name: "Host Groups", + href: "/settings/host-groups", + icon: Folder, + }, + { + name: "Agent Updates", + href: "/settings/agent-config", + icon: RefreshCw, + }, + { + name: "Agent Version", + href: "/settings/agent-version", + icon: Settings, + }, + ], + }); + } + + // Alert Management + if (canManageSettings()) { + nav.push({ + section: "Alert Management", + items: [ + { + name: "Alert Channels", + href: "/settings/alert-channels", + icon: Bell, + }, + { + name: "Notifications", + href: "/settings/notifications", + icon: Bell, + comingSoon: true, + }, + ], + }); + } + + // Server Config + if (canManageSettings()) { + nav.push({ + section: "Server", + items: [ + { + name: "URL Config", + href: "/settings/server-url", + icon: Wrench, + }, + { + name: "Server Version", + href: "/settings/server-version", + icon: Code, + }, + ], + }); + } + + return nav; + }; + + const secondaryNavigation = buildSecondaryNavigation(); + + const isActive = (path) => location.pathname === path; + + const getPageTitle = () => { + const path = location.pathname; + + if (path.startsWith("/settings/users")) return "Users"; + if (path.startsWith("/settings/host-groups")) return "Host Groups"; + if (path.startsWith("/settings/notifications")) return "Notifications"; + if (path.startsWith("/settings/agent-config")) return "Agent Config"; + if (path.startsWith("/settings/server-config")) return "Server Config"; + + return "Settings"; + }; + + return ( +
+ {/* Within-page secondary navigation and content */} +
+
+ {/* Left secondary nav (within page) */} + + + {/* Right content */} +
+
+ {children} +
+
+
+
+
+ ); +}; + +export default SettingsLayout; diff --git a/frontend/src/components/settings/AgentManagementTab.jsx b/frontend/src/components/settings/AgentManagementTab.jsx new file mode 100644 index 0000000..1c78707 --- /dev/null +++ b/frontend/src/components/settings/AgentManagementTab.jsx @@ -0,0 +1,368 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { AlertCircle, Code, Download, Plus, Shield, X } from "lucide-react"; +import { useId, useState } from "react"; +import { agentFileAPI } from "../../utils/api"; + +const AgentManagementTab = () => { + const scriptFileId = useId(); + const scriptContentId = useId(); + const [showUploadModal, setShowUploadModal] = useState(false); + + // Agent file queries and mutations + const { + data: agentFileInfo, + isLoading: agentFileLoading, + error: agentFileError, + refetch: refetchAgentFile, + } = useQuery({ + queryKey: ["agentFile"], + queryFn: () => agentFileAPI.getInfo().then((res) => res.data), + }); + + const uploadAgentMutation = useMutation({ + mutationFn: (scriptContent) => + agentFileAPI.upload(scriptContent).then((res) => res.data), + onSuccess: () => { + refetchAgentFile(); + setShowUploadModal(false); + }, + onError: (error) => { + console.error("Upload agent error:", error); + }, + }); + + return ( +
+ {/* Header */} +
+
+
+ +

+ Agent File Management +

+
+

+ Manage the PatchMon agent script file used for installations and + updates +

+
+
+ + +
+
+ + {/* Content */} + {agentFileLoading ? ( +
+
+
+ ) : agentFileError ? ( +
+

+ Error loading agent file: {agentFileError.message} +

+
+ ) : !agentFileInfo?.exists ? ( +
+ +

+ No agent script found +

+

+ Upload an agent script to get started +

+
+ ) : ( +
+ {/* Agent File Info */} +
+

+ Current Agent Script +

+
+
+ + + Version: + + + {agentFileInfo.version} + +
+
+ + + Size: + + + {agentFileInfo.sizeFormatted} + +
+
+ + + Modified: + + + {new Date(agentFileInfo.lastModified).toLocaleDateString()} + +
+
+
+ + {/* Usage Instructions */} +
+
+ +
+

+ Agent Script Usage +

+
+

This script is used for:

+
    +
  • New agent installations via the install script
  • +
  • + Agent downloads from the /api/v1/hosts/agent/download + endpoint +
  • +
  • Manual agent deployments and updates
  • +
+
+
+
+
+ + {/* Uninstall Instructions */} +
+
+ +
+

+ Agent Uninstall Command +

+
+

+ To completely remove PatchMon from a host: +

+
+
+ curl -ks {window.location.origin} + /api/v1/hosts/remove | sudo bash +
+ +
+

+ ⚠️ This will remove all PatchMon files, configuration, and + crontab entries +

+
+
+
+
+
+ )} + + {/* Agent Upload Modal */} + {showUploadModal && ( + setShowUploadModal(false)} + onSubmit={uploadAgentMutation.mutate} + isLoading={uploadAgentMutation.isPending} + error={uploadAgentMutation.error} + scriptFileId={scriptFileId} + scriptContentId={scriptContentId} + /> + )} +
+ ); +}; + +// Agent Upload Modal Component +const AgentUploadModal = ({ + isOpen, + onClose, + onSubmit, + isLoading, + error, + scriptFileId, + scriptContentId, +}) => { + const [scriptContent, setScriptContent] = useState(""); + const [uploadError, setUploadError] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + setUploadError(""); + + if (!scriptContent.trim()) { + setUploadError("Script content is required"); + return; + } + + if (!scriptContent.trim().startsWith("#!/")) { + setUploadError( + "Script must start with a shebang (#!/bin/bash or #!/bin/sh)", + ); + return; + } + + onSubmit(scriptContent); + }; + + const handleFileUpload = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + setScriptContent(event.target.result); + setUploadError(""); + }; + reader.readAsText(file); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+
+

+ Replace Agent Script +

+ +
+
+ +
+
+
+ + +

+ Select a .sh file to upload, or paste the script content below +

+
+ +
+ +