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
-
-
- toggleAutoUpdateMutation.mutate(!host.auto_update)
- }
- disabled={toggleAutoUpdateMutation.isPending}
- className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
- host.auto_update
- ? "bg-primary-600 dark:bg-primary-500"
- : "bg-secondary-200 dark:bg-secondary-600"
- }`}
- >
-
-
-
-
- )}
+
+
+ Agent Version
+
+
+ {host.agent_version || "Unknown"}
+
+
+
+
+
+ Auto-update
+
+
+ toggleAutoUpdateMutation.mutate(!host.auto_update)
+ }
+ disabled={toggleAutoUpdateMutation.isPending}
+ className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
+ host.auto_update
+ ? "bg-primary-600 dark:bg-primary-500"
+ : "bg-secondary-200 dark:bg-secondary-600"
+ }`}
+ >
+
+
+
)}
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