From 71d9884a863769828d75f2cd20a3b4089f7d1fdf Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:41:29 +0100 Subject: [PATCH 01/12] fix(frontend): imports are unused --- frontend/src/main.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 7666468..8ab4536 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,5 +1,4 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App.jsx"; From e3aa28a8d9d4a890d1fa54ef03dab2e4c99abfb5 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:57:58 +0100 Subject: [PATCH 02/12] fix: login after signup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also resolves entire user object being return to client, including password_hash... ⚠️ --- backend/src/routes/authRoutes.js | 14 +++++++++++++- .../src/components/FirstTimeAdminSetup.jsx | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js index d61934b..bcffc02 100644 --- a/backend/src/routes/authRoutes.js +++ b/backend/src/routes/authRoutes.js @@ -118,9 +118,21 @@ router.post( // Create default dashboard preferences for the new admin user await createDefaultDashboardPreferences(user.id, "admin"); + // Generate token for immediate login + const token = generateToken(user.id); + res.status(201).json({ message: "Admin user created successfully", - user: user, + token, + user: { + id: user.id, + username: user.username, + email: user.email, + role: user.role, + first_name: user.first_name, + last_name: user.last_name, + is_active: user.is_active, + }, }); } catch (error) { console.error("Error creating admin user:", error); diff --git a/frontend/src/components/FirstTimeAdminSetup.jsx b/frontend/src/components/FirstTimeAdminSetup.jsx index 38a8877..5d34624 100644 --- a/frontend/src/components/FirstTimeAdminSetup.jsx +++ b/frontend/src/components/FirstTimeAdminSetup.jsx @@ -3,7 +3,7 @@ import { useId, useState } from "react"; import { useAuth } from "../contexts/AuthContext"; const FirstTimeAdminSetup = () => { - const { login } = useAuth(); + const { login, setAuthState } = useAuth(); const firstNameId = useId(); const lastNameId = useId(); const usernameId = useId(); @@ -95,10 +95,18 @@ const FirstTimeAdminSetup = () => { if (response.ok) { setSuccess(true); - // Auto-login the user after successful setup - setTimeout(() => { - login(formData.username.trim(), formData.password); - }, 2000); + + // If the response includes a token, use it to automatically log in + if (data.token && data.user) { + // Auto-login using the token from the setup response + setAuthState(data.token, data.user); + setTimeout(() => {}, 2000); + } else { + // Fallback to manual login if no token provided + setTimeout(() => { + login(formData.username.trim(), formData.password); + }, 2000); + } } else { setError(data.error || "Failed to create admin user"); } From ba087eb23ed6429063d4cf78874c7c18a86c16b0 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:59:28 +0100 Subject: [PATCH 03/12] chore: add types/bcryptjs --- package-lock.json | 8 ++++++++ package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/package-lock.json b/package-lock.json index 4cfd878..867bb21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@biomejs/biome": "2.2.4", "@commitlint/cli": "^20.0.0", "@commitlint/config-conventional": "^20.0.0", + "@types/bcryptjs": "^2.4.6", "concurrently": "^8.2.2", "lefthook": "^1.13.4", "lint-staged": "^15.2.10", @@ -1337,6 +1338,13 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.1", "dev": true, diff --git a/package.json b/package.json index b3329b6..5f9c7bb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@biomejs/biome": "2.2.4", + "@types/bcryptjs": "^2.4.6", "@commitlint/cli": "^20.0.0", "@commitlint/config-conventional": "^20.0.0", "concurrently": "^8.2.2", From 5c6688773217d5545fda94eb3c746c21cdc6c60d Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:39:52 +0100 Subject: [PATCH 04/12] refactor(frontend): optimise auth process - Stops frontend trying to make calls that require auth before auth has occured - Stops frontend making calls that aren't necessary before auth has occured - Implements state machine to better handle auth phases --- frontend/src/App.jsx | 10 +++-- frontend/src/components/Layout.jsx | 9 ++++ frontend/src/constants/authPhases.js | 29 ++++++++++++ frontend/src/contexts/AuthContext.jsx | 45 ++++++++++++------- .../contexts/UpdateNotificationContext.jsx | 23 +++++++--- 5 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 frontend/src/constants/authPhases.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4f015f6..e6a3efd 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 { isAuthPhase } from "./constants/authPhases"; import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { ThemeProvider } from "./contexts/ThemeContext"; import { UpdateNotificationProvider } from "./contexts/UpdateNotificationContext"; @@ -20,11 +21,14 @@ import Settings from "./pages/Settings"; import Users from "./pages/Users"; function AppRoutes() { - const { needsFirstTimeSetup, checkingSetup, isAuthenticated } = useAuth(); + const { needsFirstTimeSetup, authPhase, isAuthenticated } = useAuth(); const isAuth = isAuthenticated(); // Call the function to get boolean value - // Show loading while checking if setup is needed - if (checkingSetup) { + // Show loading while checking setup or initialising + if ( + isAuthPhase.initialising(authPhase) || + isAuthPhase.checkingSetup(authPhase) + ) { return (
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index fcda7d2..c5952e6 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -241,6 +241,14 @@ const Layout = ({ children }) => { // Fetch GitHub stars count const fetchGitHubStars = useCallback(async () => { + // Skip if already fetched recently + const lastFetch = localStorage.getItem("githubStarsFetchTime"); + const now = Date.now(); + if (lastFetch && now - parseInt(lastFetch, 15) < 600000) { + // 15 minute cache + return; + } + try { const response = await fetch( "https://api.github.com/repos/9technologygroup/patchmon.net", @@ -248,6 +256,7 @@ const Layout = ({ children }) => { if (response.ok) { const data = await response.json(); setGithubStars(data.stargazers_count); + localStorage.setItem("githubStarsFetchTime", now.toString()); } } catch (error) { console.error("Failed to fetch GitHub stars:", error); diff --git a/frontend/src/constants/authPhases.js b/frontend/src/constants/authPhases.js new file mode 100644 index 0000000..e5acecf --- /dev/null +++ b/frontend/src/constants/authPhases.js @@ -0,0 +1,29 @@ +/** + * Authentication phases for the centralized auth state machine + * + * Flow: INITIALISING → CHECKING_SETUP → READY + */ +export const AUTH_PHASES = { + INITIALISING: "INITIALISING", + CHECKING_SETUP: "CHECKING_SETUP", + READY: "READY", +}; + +/** + * Helper functions for auth phase management + */ +export const isAuthPhase = { + initialising: (phase) => phase === AUTH_PHASES.INITIALISING, + checkingSetup: (phase) => phase === AUTH_PHASES.CHECKING_SETUP, + ready: (phase) => phase === AUTH_PHASES.READY, +}; + +/** + * Check if authentication is fully initialised and ready + * @param {string} phase - Current auth phase + * @param {boolean} isAuthenticated - Whether user is authenticated + * @returns {boolean} - True if auth is ready for other contexts to use + */ +export const isAuthReady = (phase, isAuthenticated) => { + return isAuthPhase.ready(phase) && isAuthenticated; +}; diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 17357e8..bf2cf87 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -5,6 +5,7 @@ import { useEffect, useState, } from "react"; +import { AUTH_PHASES, isAuthPhase } from "../constants/authPhases"; const AuthContext = createContext(); @@ -20,11 +21,11 @@ export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [permissions, setPermissions] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [permissionsLoading, setPermissionsLoading] = useState(false); const [needsFirstTimeSetup, setNeedsFirstTimeSetup] = useState(false); - const [checkingSetup, setCheckingSetup] = useState(true); + // Authentication state machine phases + const [authPhase, setAuthPhase] = useState(AUTH_PHASES.INITIALISING); + const [permissionsLoading, setPermissionsLoading] = useState(false); // Define functions first const fetchPermissions = useCallback(async (authToken) => { @@ -77,14 +78,20 @@ export const AuthProvider = ({ children }) => { // Use the proper fetchPermissions function fetchPermissions(storedToken); } + // User is authenticated, skip setup check + setAuthPhase(AUTH_PHASES.READY); } catch (error) { console.error("Error parsing stored user data:", error); localStorage.removeItem("token"); localStorage.removeItem("user"); localStorage.removeItem("permissions"); + // Move to setup check phase + setAuthPhase(AUTH_PHASES.CHECKING_SETUP); } + } else { + // No stored auth, check if setup is needed + setAuthPhase(AUTH_PHASES.CHECKING_SETUP); } - setIsLoading(false); }, [fetchPermissions]); // Refresh permissions when user logs in (no automatic refresh) @@ -202,10 +209,6 @@ export const AuthProvider = ({ children }) => { } }; - const isAuthenticated = () => { - return !!(token && user); - }; - const isAdmin = () => { return user?.role === "admin"; }; @@ -243,42 +246,50 @@ export const AuthProvider = ({ children }) => { if (response.ok) { const data = await response.json(); setNeedsFirstTimeSetup(!data.hasAdminUsers); + setAuthPhase(AUTH_PHASES.READY); // Setup check complete, move to ready phase } else { // If endpoint doesn't exist or fails, assume setup is needed setNeedsFirstTimeSetup(true); + setAuthPhase(AUTH_PHASES.READY); } } catch (error) { console.error("Error checking admin users:", error); // If there's an error, assume setup is needed setNeedsFirstTimeSetup(true); - } finally { - setCheckingSetup(false); + setAuthPhase(AUTH_PHASES.READY); } }, []); - // Check for admin users on initial load + // Check for admin users ONLY when in CHECKING_SETUP phase useEffect(() => { - if (!token && !user) { + if (isAuthPhase.checkingSetup(authPhase)) { checkAdminUsersExist(); - } else { - setCheckingSetup(false); } - }, [token, user, checkAdminUsersExist]); + }, [authPhase, checkAdminUsersExist]); const setAuthState = (authToken, authUser) => { setToken(authToken); setUser(authUser); localStorage.setItem("token", authToken); localStorage.setItem("user", JSON.stringify(authUser)); + setAuthPhase(AUTH_PHASES.READY); // Authentication complete, move to ready phase + }; + + // Computed loading state based on phase and permissions state + const isLoading = !isAuthPhase.ready(authPhase) || permissionsLoading; + + // Function to check authentication status (maintains API compatibility) + const isAuthenticated = () => { + return !!(user && token && isAuthPhase.ready(authPhase)); }; const value = { user, token, permissions, - isLoading: isLoading || permissionsLoading || checkingSetup, + isLoading, needsFirstTimeSetup, - checkingSetup, + authPhase, login, logout, updateProfile, diff --git a/frontend/src/contexts/UpdateNotificationContext.jsx b/frontend/src/contexts/UpdateNotificationContext.jsx index 84d2fe0..cb57f5d 100644 --- a/frontend/src/contexts/UpdateNotificationContext.jsx +++ b/frontend/src/contexts/UpdateNotificationContext.jsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { createContext, useContext, useState } from "react"; +import { createContext, useContext, useMemo, useState } from "react"; +import { isAuthReady } from "../constants/authPhases"; import { settingsAPI, versionAPI } from "../utils/api"; import { useAuth } from "./AuthContext"; @@ -17,16 +18,26 @@ export const useUpdateNotification = () => { export const UpdateNotificationProvider = ({ children }) => { const [dismissed, setDismissed] = useState(false); - const { user, token } = useAuth(); + const { authPhase, isAuthenticated } = useAuth(); - // Ensure settings are loaded + // Ensure settings are loaded - but only after auth is fully ready const { data: settings, isLoading: settingsLoading } = useQuery({ queryKey: ["settings"], queryFn: () => settingsAPI.get().then((res) => res.data), - enabled: !!(user && token), - retry: 1, + staleTime: 5 * 60 * 1000, // Settings stay fresh for 5 minutes + refetchOnWindowFocus: false, + enabled: isAuthReady(authPhase, isAuthenticated()), }); + // Memoize the enabled condition to prevent unnecessary re-evaluations + const isQueryEnabled = useMemo(() => { + return ( + isAuthReady(authPhase, isAuthenticated()) && + !!settings && + !settingsLoading + ); + }, [authPhase, isAuthenticated, settings, settingsLoading]); + // Query for update information const { data: updateData, @@ -38,7 +49,7 @@ export const UpdateNotificationProvider = ({ children }) => { staleTime: 10 * 60 * 1000, // Data stays fresh for 10 minutes refetchOnWindowFocus: false, // Don't refetch when window regains focus retry: 1, - enabled: !!(user && token && settings && !settingsLoading), // Only run when authenticated and settings are loaded + enabled: isQueryEnabled, }); const updateAvailable = updateData?.isUpdateAvailable && !dismissed; From 60fa59880334f26cae452075573d19fd90dcac60 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:59:56 +0100 Subject: [PATCH 05/12] fix(frontend): solve missing imports --- frontend/src/main.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 8ab4536..4136b64 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,4 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App.jsx"; @@ -16,11 +17,11 @@ const queryClient = new QueryClient({ }); ReactDOM.createRoot(document.getElementById("root")).render( - + - , + , ); From c4e056711b231fdeb7b794c3ed7a5d03711d3bc2 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:00:35 +0100 Subject: [PATCH 06/12] fix(frontend): use React Router navigation to open add host modal Fixes page reload on add host button click. --- frontend/src/components/Layout.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index c5952e6..90f212c 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -28,7 +28,7 @@ import { X, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Link, useLocation } from "react-router-dom"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { useUpdateNotification } from "../contexts/UpdateNotificationContext"; import { dashboardAPI, versionAPI } from "../utils/api"; @@ -44,6 +44,7 @@ const Layout = ({ children }) => { const [_userMenuOpen, setUserMenuOpen] = useState(false); const [githubStars, setGithubStars] = useState(null); const location = useLocation(); + const navigate = useNavigate(); const { user, logout, @@ -236,7 +237,7 @@ const Layout = ({ children }) => { const handleAddHost = () => { // Navigate to hosts page with add modal parameter - window.location.href = "/hosts?action=add"; + navigate("/hosts?action=add"); }; // Fetch GitHub stars count From be3fe52aeaa516b814dae3bd70f0f3ddf369641f Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:57:01 +0100 Subject: [PATCH 07/12] fix(api): resolve duplicate key constraint errors in host package updates --- backend/src/routes/hostRoutes.js | 62 ++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 444e3ef..88f76d3 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -375,14 +375,21 @@ router.post( updateData.status = "active"; } - await prisma.hosts.update({ - where: { id: host.id }, - data: updateData, - }); + // Calculate package counts before transaction + const securityCount = packages.filter( + (pkg) => pkg.isSecurityUpdate, + ).length; + const updatesCount = packages.filter((pkg) => pkg.needsUpdate).length; - // Process packages in transaction + // Process everything in a single transaction to avoid race conditions await prisma.$transaction(async (tx) => { - // Clear existing host packages + // Update host data + await tx.hosts.update({ + where: { id: host.id }, + data: updateData, + }); + + // Clear existing host packages to avoid duplicates await tx.host_packages.deleteMany({ where: { host_id: host.id }, }); @@ -423,8 +430,22 @@ router.post( } // Create host package relationship - await tx.host_packages.create({ - data: { + // Use upsert to handle potential duplicates gracefully + await tx.host_packages.upsert({ + where: { + host_id_package_id: { + host_id: host.id, + package_id: pkg.id, + }, + }, + update: { + current_version: packageData.currentVersion, + available_version: packageData.availableVersion || null, + needs_update: packageData.needsUpdate, + is_security_update: packageData.isSecurityUpdate || false, + last_checked: new Date(), + }, + create: { id: uuidv4(), host_id: host.id, package_id: pkg.id, @@ -493,22 +514,17 @@ router.post( }); } } - }); - // Create update history record - const securityCount = packages.filter( - (pkg) => pkg.isSecurityUpdate, - ).length; - const updatesCount = packages.filter((pkg) => pkg.needsUpdate).length; - - await prisma.update_history.create({ - data: { - id: uuidv4(), - host_id: host.id, - packages_count: updatesCount, - security_count: securityCount, - status: "success", - }, + // Create update history record + await tx.update_history.create({ + data: { + id: uuidv4(), + host_id: host.id, + packages_count: updatesCount, + security_count: securityCount, + status: "success", + }, + }); }); // Check if auto-update is enabled and if there's a newer agent version available From c886b812d6915556433ee7f468afefda673feff1 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Fri, 26 Sep 2025 23:24:34 +0100 Subject: [PATCH 08/12] chore: clarify auto-update feature --- agents/patchmon-agent.sh | 12 ++--- agents/patchmon_install.sh | 6 +-- backend/src/routes/hostRoutes.js | 20 ++++---- frontend/src/pages/HostDetail.jsx | 79 +++++++++++++++---------------- frontend/src/pages/Hosts.jsx | 7 ++- 5 files changed, 62 insertions(+), 62 deletions(-) diff --git a/agents/patchmon-agent.sh b/agents/patchmon-agent.sh index 7749d85..364dda2 100755 --- a/agents/patchmon-agent.sh +++ b/agents/patchmon-agent.sh @@ -821,7 +821,7 @@ EOF success "Update sent successfully" echo "$response" | grep -o '"packagesProcessed":[0-9]*' | cut -d':' -f2 | xargs -I {} info "Processed {} packages" - # Check for auto-update instructions (look specifically in autoUpdate section) + # Check for PatchMon agent update instructions (this updates the agent script, not system packages) if echo "$response" | grep -q '"autoUpdate":{'; then local auto_update_section=$(echo "$response" | grep -o '"autoUpdate":{[^}]*}') local should_update=$(echo "$auto_update_section" | grep -o '"shouldUpdate":true' | cut -d':' -f2) @@ -830,15 +830,15 @@ EOF local current_version=$(echo "$auto_update_section" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4) local update_message=$(echo "$auto_update_section" | grep -o '"message":"[^"]*' | cut -d'"' -f4) - info "Auto-update detected: $update_message" + info "PatchMon agent update detected: $update_message" info "Current version: $current_version, Latest version: $latest_version" - # Automatically run update-agent command - info "Automatically updating agent to latest version..." + # Automatically run update-agent command to update the PatchMon agent script + info "Automatically updating PatchMon agent to latest version..." if "$0" update-agent; then - success "Agent auto-update completed successfully" + success "PatchMon agent update completed successfully" else - warning "Agent auto-update failed, but data was sent successfully" + warning "PatchMon agent update failed, but data was sent successfully" fi fi fi diff --git a/agents/patchmon_install.sh b/agents/patchmon_install.sh index 7a5feac..7c30d6a 100644 --- a/agents/patchmon_install.sh +++ b/agents/patchmon_install.sh @@ -158,8 +158,8 @@ else warning "Initial package data failed, but agent is configured. You can run 'patchmon-agent.sh update' manually." fi -# Setup crontab for automatic updates -info "⏰ Setting up automatic updates every $UPDATE_INTERVAL minutes..." +# Setup crontab for automatic package status updates +info "⏰ Setting up automatic package status update every $UPDATE_INTERVAL minutes..." if [[ $UPDATE_INTERVAL -eq 60 ]]; then # Hourly updates echo "0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | crontab - @@ -179,7 +179,7 @@ if [[ "$EXPECTED_VERSION" != "Unknown" ]]; then fi echo " • Config directory: /etc/patchmon/" echo " • Credentials file: /etc/patchmon/credentials" -echo " • Automatic updates: Every $UPDATE_INTERVAL minutes via crontab" +echo " • Status updates: Every $UPDATE_INTERVAL minutes via crontab" echo " • View logs: tail -f /var/log/patchmon-agent.log" echo "" echo "🔧 Manual commands:" diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 88f76d3..0317e88 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -527,11 +527,11 @@ router.post( }); }); - // Check if auto-update is enabled and if there's a newer agent version available + // Check if agent auto-update is enabled and if there's a newer version available let autoUpdateResponse = null; try { const settings = await prisma.settings.findFirst(); - // Check both global auto-update setting AND host-specific auto-update setting + // Check both global agent auto-update setting AND host-specific agent auto-update setting if (settings?.auto_update && host.auto_update) { // Get current agent version from the request const currentAgentVersion = req.body.agentVersion; @@ -560,8 +560,8 @@ router.post( } } } catch (error) { - console.error("Auto-update check error:", error); - // Don't fail the update if auto-update check fails + console.error("Agent auto-update check error:", error); + // Don't fail the update if agent auto-update check fails } const response = { @@ -571,7 +571,7 @@ router.post( securityUpdates: securityCount, }; - // Add auto-update response if available + // Add agent auto-update response if available if (autoUpdateResponse) { response.autoUpdate = autoUpdateResponse; } @@ -1060,7 +1060,7 @@ router.delete( }, ); -// Toggle host auto-update setting +// Toggle agent auto-update setting router.patch( "/:hostId/auto-update", authenticateToken, @@ -1068,7 +1068,7 @@ router.patch( [ body("auto_update") .isBoolean() - .withMessage("Auto-update must be a boolean"), + .withMessage("Agent auto-update setting must be a boolean"), ], async (req, res) => { try { @@ -1089,7 +1089,7 @@ router.patch( }); res.json({ - message: `Host auto-update ${auto_update ? "enabled" : "disabled"} successfully`, + message: `Agent auto-update ${auto_update ? "enabled" : "disabled"} successfully`, host: { id: host.id, friendlyName: host.friendly_name, @@ -1097,8 +1097,8 @@ router.patch( }, }); } catch (error) { - console.error("Host auto-update toggle error:", error); - res.status(500).json({ error: "Failed to toggle host auto-update" }); + console.error("Agent auto-update toggle error:", error); + res.status(500).json({ error: "Failed to toggle agent auto-update" }); } }, ); diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index 058385b..9deba67 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -84,7 +84,7 @@ const HostDetail = () => { }, }); - // Toggle auto-update mutation + // Toggle agent auto-update mutation (updates PatchMon agent script, not system packages) const toggleAutoUpdateMutation = useMutation({ mutationFn: (auto_update) => adminHostsAPI @@ -350,7 +350,7 @@ const HostDetail = () => {
-

+

Friendly Name

{ {host.hostname && (
-

+

System Hostname

@@ -384,7 +384,7 @@ const HostDetail = () => { )}

-

+

Host Group

{host.host_groups ? ( @@ -402,7 +402,7 @@ const HostDetail = () => {
-

+

Operating System

@@ -413,43 +413,38 @@ const HostDetail = () => {
- {host.agent_version && ( -
-
-

- Agent Version -

-

- {host.agent_version} -

-
-
- - Auto-update - - -
-
- )} +
+

+ Agent Version +

+

+ {host.agent_version || "Unknown"} +

+
+ +
+

+ Auto-update +

+ +
)} diff --git a/frontend/src/pages/Hosts.jsx b/frontend/src/pages/Hosts.jsx index 891961a..1d4ec9e 100644 --- a/frontend/src/pages/Hosts.jsx +++ b/frontend/src/pages/Hosts.jsx @@ -326,7 +326,12 @@ const Hosts = () => { { id: "os", label: "OS", visible: true, order: 4 }, { id: "os_version", label: "OS Version", visible: false, order: 5 }, { id: "agent_version", label: "Agent Version", visible: true, order: 6 }, - { id: "auto_update", label: "Auto-update", visible: true, order: 7 }, + { + id: "auto_update", + label: "Agent Auto-Update", + visible: true, + order: 7, + }, { id: "status", label: "Status", visible: true, order: 8 }, { id: "updates", label: "Updates", visible: true, order: 9 }, { id: "last_update", label: "Last Update", visible: true, order: 10 }, From 751a202fec817bef920bcc733c350931e1bc5f06 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Sat, 27 Sep 2025 00:41:34 +0100 Subject: [PATCH 09/12] fix(frontend): actually fix login after signup --- .../src/components/FirstTimeAdminSetup.jsx | 25 ++++++++++++++----- frontend/src/contexts/AuthContext.jsx | 16 +++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/FirstTimeAdminSetup.jsx b/frontend/src/components/FirstTimeAdminSetup.jsx index 5d34624..09e1f97 100644 --- a/frontend/src/components/FirstTimeAdminSetup.jsx +++ b/frontend/src/components/FirstTimeAdminSetup.jsx @@ -1,9 +1,11 @@ import { AlertCircle, CheckCircle, Shield, UserPlus } from "lucide-react"; import { useId, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; const FirstTimeAdminSetup = () => { const { login, setAuthState } = useAuth(); + const navigate = useNavigate(); const firstNameId = useId(); const lastNameId = useId(); const usernameId = useId(); @@ -98,13 +100,24 @@ const FirstTimeAdminSetup = () => { // If the response includes a token, use it to automatically log in if (data.token && data.user) { - // Auto-login using the token from the setup response + // Set the authentication state immediately setAuthState(data.token, data.user); - setTimeout(() => {}, 2000); + // Navigate to dashboard after successful setup + setTimeout(() => { + navigate("/", { replace: true }); + }, 100); // Small delay to ensure auth state is set } else { // Fallback to manual login if no token provided - setTimeout(() => { - login(formData.username.trim(), formData.password); + setTimeout(async () => { + try { + await login(formData.username.trim(), formData.password); + } catch (error) { + console.error("Auto-login failed:", error); + setError( + "Account created but auto-login failed. Please login manually.", + ); + setSuccess(false); + } }, 2000); } } else { @@ -132,8 +145,8 @@ const FirstTimeAdminSetup = () => { Admin Account Created!

- Your admin account has been successfully created. You will be - automatically logged in shortly. + Your admin account has been successfully created and you are now + logged in. Redirecting to the dashboard...

diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index bf2cf87..cc86e6e 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -5,6 +5,7 @@ import { useEffect, useState, } from "react"; +import { flushSync } from "react-dom"; import { AUTH_PHASES, isAuthPhase } from "../constants/authPhases"; const AuthContext = createContext(); @@ -268,11 +269,20 @@ export const AuthProvider = ({ children }) => { }, [authPhase, checkAdminUsersExist]); const setAuthState = (authToken, authUser) => { - setToken(authToken); - setUser(authUser); + // Use flushSync to ensure all state updates are applied synchronously + flushSync(() => { + setToken(authToken); + setUser(authUser); + setNeedsFirstTimeSetup(false); + setAuthPhase(AUTH_PHASES.READY); + }); + + // Store in localStorage after state is updated localStorage.setItem("token", authToken); localStorage.setItem("user", JSON.stringify(authUser)); - setAuthPhase(AUTH_PHASES.READY); // Authentication complete, move to ready phase + + // Fetch permissions immediately for the new authenticated user + fetchPermissions(authToken); }; // Computed loading state based on phase and permissions state From 102546e45dbce450f23c1e5297bf6e27a64d71c2 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Sat, 27 Sep 2025 01:49:33 +0100 Subject: [PATCH 10/12] fix(frontend): eliminate duplicate API calls during log in/out --- frontend/src/contexts/AuthContext.jsx | 14 ++++++-------- frontend/src/pages/Login.jsx | 18 ++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index cc86e6e..7f31e12 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -95,14 +95,6 @@ export const AuthProvider = ({ children }) => { } }, [fetchPermissions]); - // Refresh permissions when user logs in (no automatic refresh) - useEffect(() => { - if (token && user) { - // Only refresh permissions once when user logs in - refreshPermissions(); - } - }, [token, user, refreshPermissions]); - const login = async (username, password) => { try { const response = await fetch("/api/v1/auth/login", { @@ -116,6 +108,12 @@ export const AuthProvider = ({ children }) => { const data = await response.json(); if (response.ok) { + // Check if TFA is required + if (data.requiresTfa) { + return { success: true, requiresTfa: true }; + } + + // Regular successful login setToken(data.token); setUser(data.user); localStorage.setItem("token", data.token); diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 4c9cb2f..a0c76a3 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -67,23 +67,17 @@ const Login = () => { setError(""); try { - const response = await authAPI.login( - formData.username, - formData.password, - ); + // Use the AuthContext login function which handles everything + const result = await login(formData.username, formData.password); - if (response.data.requiresTfa) { + if (result.requiresTfa) { setRequiresTfa(true); setTfaUsername(formData.username); setError(""); + } else if (result.success) { + navigate("/"); } else { - // Regular login successful - const result = await login(formData.username, formData.password); - if (result.success) { - navigate("/"); - } else { - setError(result.error || "Login failed"); - } + setError(result.error || "Login failed"); } } catch (err) { setError(err.response?.data?.error || "Login failed"); From 175042690e738ecdebf3d656c3d0ab9ed6c518c9 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Sat, 27 Sep 2025 02:14:36 +0100 Subject: [PATCH 11/12] refactor(frontend): don't store permissions in localstorage --- frontend/src/contexts/AuthContext.jsx | 12 ++---------- frontend/src/utils/api.js | 1 - 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 7f31e12..b7d7cc9 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -41,7 +41,6 @@ export const AuthProvider = ({ children }) => { if (response.ok) { const data = await response.json(); setPermissions(data); - localStorage.setItem("permissions", JSON.stringify(data)); return data; } else { console.error("Failed to fetch permissions"); @@ -67,25 +66,19 @@ export const AuthProvider = ({ children }) => { useEffect(() => { const storedToken = localStorage.getItem("token"); const storedUser = localStorage.getItem("user"); - const storedPermissions = localStorage.getItem("permissions"); if (storedToken && storedUser) { try { setToken(storedToken); setUser(JSON.parse(storedUser)); - if (storedPermissions) { - setPermissions(JSON.parse(storedPermissions)); - } else { - // Use the proper fetchPermissions function - fetchPermissions(storedToken); - } + // Fetch permissions from backend + fetchPermissions(storedToken); // User is authenticated, skip setup check setAuthPhase(AUTH_PHASES.READY); } catch (error) { console.error("Error parsing stored user data:", error); localStorage.removeItem("token"); localStorage.removeItem("user"); - localStorage.removeItem("permissions"); // Move to setup check phase setAuthPhase(AUTH_PHASES.CHECKING_SETUP); } @@ -153,7 +146,6 @@ export const AuthProvider = ({ children }) => { setPermissions(null); localStorage.removeItem("token"); localStorage.removeItem("user"); - localStorage.removeItem("permissions"); } }; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index afccb1c..c226fa9 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -39,7 +39,6 @@ api.interceptors.response.use( // Handle unauthorized localStorage.removeItem("token"); localStorage.removeItem("user"); - localStorage.removeItem("permissions"); window.location.href = "/login"; } } From f99e01a1203155d7c7ccd24fcb18cf7e06e453c0 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Sat, 27 Sep 2025 02:18:09 +0100 Subject: [PATCH 12/12] refactor(frontend): don't store current host tab in localstorage --- frontend/src/pages/HostDetail.jsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index 9deba67..1e3a601 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -44,11 +44,7 @@ const HostDetail = () => { const [showCredentialsModal, setShowCredentialsModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showAllUpdates, setShowAllUpdates] = useState(false); - const [activeTab, setActiveTab] = useState(() => { - // Restore tab state from localStorage - const savedTab = localStorage.getItem(`host-detail-tab-${hostId}`); - return savedTab || "host"; - }); + const [activeTab, setActiveTab] = useState("host"); const { data: host, @@ -63,10 +59,9 @@ const HostDetail = () => { refetchOnWindowFocus: false, // Don't refetch when window regains focus }); - // Save tab state to localStorage when it changes + // Tab change handler const handleTabChange = (tabName) => { setActiveTab(tabName); - localStorage.setItem(`host-detail-tab-${hostId}`, tabName); }; // Auto-show credentials modal for new/pending hosts