mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-26 09:33:40 +00:00
Merge remote-tracking branch 'origin/v1-2-7' into v1-2-7
This commit is contained in:
@@ -821,7 +821,7 @@ EOF
|
|||||||
success "Update sent successfully"
|
success "Update sent successfully"
|
||||||
echo "$response" | grep -o '"packagesProcessed":[0-9]*' | cut -d':' -f2 | xargs -I {} info "Processed {} packages"
|
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
|
if echo "$response" | grep -q '"autoUpdate":{'; then
|
||||||
local auto_update_section=$(echo "$response" | grep -o '"autoUpdate":{[^}]*}')
|
local auto_update_section=$(echo "$response" | grep -o '"autoUpdate":{[^}]*}')
|
||||||
local should_update=$(echo "$auto_update_section" | grep -o '"shouldUpdate":true' | cut -d':' -f2)
|
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 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)
|
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"
|
info "Current version: $current_version, Latest version: $latest_version"
|
||||||
|
|
||||||
# Automatically run update-agent command
|
# Automatically run update-agent command to update the PatchMon agent script
|
||||||
info "Automatically updating agent to latest version..."
|
info "Automatically updating PatchMon agent to latest version..."
|
||||||
if "$0" update-agent; then
|
if "$0" update-agent; then
|
||||||
success "Agent auto-update completed successfully"
|
success "PatchMon agent update completed successfully"
|
||||||
else
|
else
|
||||||
warning "Agent auto-update failed, but data was sent successfully"
|
warning "PatchMon agent update failed, but data was sent successfully"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ else
|
|||||||
warning "Initial package data failed, but agent is configured. You can run 'patchmon-agent.sh update' manually."
|
warning "Initial package data failed, but agent is configured. You can run 'patchmon-agent.sh update' manually."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Setup crontab for automatic updates
|
# Setup crontab for automatic package status updates
|
||||||
info "⏰ Setting up automatic updates every $UPDATE_INTERVAL minutes..."
|
info "⏰ Setting up automatic package status update every $UPDATE_INTERVAL minutes..."
|
||||||
if [[ $UPDATE_INTERVAL -eq 60 ]]; then
|
if [[ $UPDATE_INTERVAL -eq 60 ]]; then
|
||||||
# Hourly updates
|
# Hourly updates
|
||||||
echo "0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | crontab -
|
echo "0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | crontab -
|
||||||
@@ -179,7 +179,7 @@ if [[ "$EXPECTED_VERSION" != "Unknown" ]]; then
|
|||||||
fi
|
fi
|
||||||
echo " • Config directory: /etc/patchmon/"
|
echo " • Config directory: /etc/patchmon/"
|
||||||
echo " • Credentials file: /etc/patchmon/credentials"
|
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 " • View logs: tail -f /var/log/patchmon-agent.log"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔧 Manual commands:"
|
echo "🔧 Manual commands:"
|
||||||
|
|||||||
@@ -118,9 +118,21 @@ router.post(
|
|||||||
// Create default dashboard preferences for the new admin user
|
// Create default dashboard preferences for the new admin user
|
||||||
await createDefaultDashboardPreferences(user.id, "admin");
|
await createDefaultDashboardPreferences(user.id, "admin");
|
||||||
|
|
||||||
|
// Generate token for immediate login
|
||||||
|
const token = generateToken(user.id);
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
message: "Admin user created successfully",
|
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) {
|
} catch (error) {
|
||||||
console.error("Error creating admin user:", error);
|
console.error("Error creating admin user:", error);
|
||||||
|
|||||||
@@ -375,14 +375,21 @@ router.post(
|
|||||||
updateData.status = "active";
|
updateData.status = "active";
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.hosts.update({
|
// Calculate package counts before transaction
|
||||||
where: { id: host.id },
|
const securityCount = packages.filter(
|
||||||
data: updateData,
|
(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) => {
|
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({
|
await tx.host_packages.deleteMany({
|
||||||
where: { host_id: host.id },
|
where: { host_id: host.id },
|
||||||
});
|
});
|
||||||
@@ -423,8 +430,22 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create host package relationship
|
// Create host package relationship
|
||||||
await tx.host_packages.create({
|
// Use upsert to handle potential duplicates gracefully
|
||||||
data: {
|
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(),
|
id: uuidv4(),
|
||||||
host_id: host.id,
|
host_id: host.id,
|
||||||
package_id: pkg.id,
|
package_id: pkg.id,
|
||||||
@@ -493,29 +514,24 @@ router.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create update history record
|
||||||
|
await tx.update_history.create({
|
||||||
|
data: {
|
||||||
|
id: uuidv4(),
|
||||||
|
host_id: host.id,
|
||||||
|
packages_count: updatesCount,
|
||||||
|
security_count: securityCount,
|
||||||
|
status: "success",
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create update history record
|
// Check if agent auto-update is enabled and if there's a newer version available
|
||||||
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",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if auto-update is enabled and if there's a newer agent version available
|
|
||||||
let autoUpdateResponse = null;
|
let autoUpdateResponse = null;
|
||||||
try {
|
try {
|
||||||
const settings = await prisma.settings.findFirst();
|
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) {
|
if (settings?.auto_update && host.auto_update) {
|
||||||
// Get current agent version from the request
|
// Get current agent version from the request
|
||||||
const currentAgentVersion = req.body.agentVersion;
|
const currentAgentVersion = req.body.agentVersion;
|
||||||
@@ -544,8 +560,8 @@ router.post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Auto-update check error:", error);
|
console.error("Agent auto-update check error:", error);
|
||||||
// Don't fail the update if auto-update check fails
|
// Don't fail the update if agent auto-update check fails
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
@@ -555,7 +571,7 @@ router.post(
|
|||||||
securityUpdates: securityCount,
|
securityUpdates: securityCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add auto-update response if available
|
// Add agent auto-update response if available
|
||||||
if (autoUpdateResponse) {
|
if (autoUpdateResponse) {
|
||||||
response.autoUpdate = autoUpdateResponse;
|
response.autoUpdate = autoUpdateResponse;
|
||||||
}
|
}
|
||||||
@@ -1044,7 +1060,7 @@ router.delete(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toggle host auto-update setting
|
// Toggle agent auto-update setting
|
||||||
router.patch(
|
router.patch(
|
||||||
"/:hostId/auto-update",
|
"/:hostId/auto-update",
|
||||||
authenticateToken,
|
authenticateToken,
|
||||||
@@ -1052,7 +1068,7 @@ router.patch(
|
|||||||
[
|
[
|
||||||
body("auto_update")
|
body("auto_update")
|
||||||
.isBoolean()
|
.isBoolean()
|
||||||
.withMessage("Auto-update must be a boolean"),
|
.withMessage("Agent auto-update setting must be a boolean"),
|
||||||
],
|
],
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -1073,7 +1089,7 @@ router.patch(
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `Host auto-update ${auto_update ? "enabled" : "disabled"} successfully`,
|
message: `Agent auto-update ${auto_update ? "enabled" : "disabled"} successfully`,
|
||||||
host: {
|
host: {
|
||||||
id: host.id,
|
id: host.id,
|
||||||
friendlyName: host.friendly_name,
|
friendlyName: host.friendly_name,
|
||||||
@@ -1081,8 +1097,8 @@ router.patch(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Host auto-update toggle error:", error);
|
console.error("Agent auto-update toggle error:", error);
|
||||||
res.status(500).json({ error: "Failed to toggle host auto-update" });
|
res.status(500).json({ error: "Failed to toggle agent auto-update" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Route, Routes } from "react-router-dom";
|
|||||||
import FirstTimeAdminSetup from "./components/FirstTimeAdminSetup";
|
import FirstTimeAdminSetup from "./components/FirstTimeAdminSetup";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
import ProtectedRoute from "./components/ProtectedRoute";
|
import ProtectedRoute from "./components/ProtectedRoute";
|
||||||
|
import { isAuthPhase } from "./constants/authPhases";
|
||||||
import { AuthProvider, useAuth } from "./contexts/AuthContext";
|
import { AuthProvider, useAuth } from "./contexts/AuthContext";
|
||||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import { UpdateNotificationProvider } from "./contexts/UpdateNotificationContext";
|
import { UpdateNotificationProvider } from "./contexts/UpdateNotificationContext";
|
||||||
@@ -20,11 +21,14 @@ import Settings from "./pages/Settings";
|
|||||||
import Users from "./pages/Users";
|
import Users from "./pages/Users";
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
const { needsFirstTimeSetup, checkingSetup, isAuthenticated } = useAuth();
|
const { needsFirstTimeSetup, authPhase, isAuthenticated } = useAuth();
|
||||||
const isAuth = isAuthenticated(); // Call the function to get boolean value
|
const isAuth = isAuthenticated(); // Call the function to get boolean value
|
||||||
|
|
||||||
// Show loading while checking if setup is needed
|
// Show loading while checking setup or initialising
|
||||||
if (checkingSetup) {
|
if (
|
||||||
|
isAuthPhase.initialising(authPhase) ||
|
||||||
|
isAuthPhase.checkingSetup(authPhase)
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-secondary-50 dark:from-secondary-900 dark:to-secondary-800 flex items-center justify-center">
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-secondary-50 dark:from-secondary-900 dark:to-secondary-800 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { AlertCircle, CheckCircle, Shield, UserPlus } from "lucide-react";
|
import { AlertCircle, CheckCircle, Shield, UserPlus } from "lucide-react";
|
||||||
import { useId, useState } from "react";
|
import { useId, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
const FirstTimeAdminSetup = () => {
|
const FirstTimeAdminSetup = () => {
|
||||||
const { login } = useAuth();
|
const { login, setAuthState } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
const firstNameId = useId();
|
const firstNameId = useId();
|
||||||
const lastNameId = useId();
|
const lastNameId = useId();
|
||||||
const usernameId = useId();
|
const usernameId = useId();
|
||||||
@@ -95,10 +97,29 @@ const FirstTimeAdminSetup = () => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
// Auto-login the user after successful setup
|
|
||||||
setTimeout(() => {
|
// If the response includes a token, use it to automatically log in
|
||||||
login(formData.username.trim(), formData.password);
|
if (data.token && data.user) {
|
||||||
}, 2000);
|
// Set the authentication state immediately
|
||||||
|
setAuthState(data.token, data.user);
|
||||||
|
// 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(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 {
|
} else {
|
||||||
setError(data.error || "Failed to create admin user");
|
setError(data.error || "Failed to create admin user");
|
||||||
}
|
}
|
||||||
@@ -124,8 +145,8 @@ const FirstTimeAdminSetup = () => {
|
|||||||
Admin Account Created!
|
Admin Account Created!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-secondary-600 dark:text-secondary-300 mb-6">
|
<p className="text-secondary-600 dark:text-secondary-300 mb-6">
|
||||||
Your admin account has been successfully created. You will be
|
Your admin account has been successfully created and you are now
|
||||||
automatically logged in shortly.
|
logged in. Redirecting to the dashboard...
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-600"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-600"></div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "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 { useAuth } from "../contexts/AuthContext";
|
||||||
import { useUpdateNotification } from "../contexts/UpdateNotificationContext";
|
import { useUpdateNotification } from "../contexts/UpdateNotificationContext";
|
||||||
import { dashboardAPI, versionAPI } from "../utils/api";
|
import { dashboardAPI, versionAPI } from "../utils/api";
|
||||||
@@ -44,6 +44,7 @@ const Layout = ({ children }) => {
|
|||||||
const [_userMenuOpen, setUserMenuOpen] = useState(false);
|
const [_userMenuOpen, setUserMenuOpen] = useState(false);
|
||||||
const [githubStars, setGithubStars] = useState(null);
|
const [githubStars, setGithubStars] = useState(null);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
logout,
|
logout,
|
||||||
@@ -236,11 +237,19 @@ const Layout = ({ children }) => {
|
|||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
// Navigate to hosts page with add modal parameter
|
// Navigate to hosts page with add modal parameter
|
||||||
window.location.href = "/hosts?action=add";
|
navigate("/hosts?action=add");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch GitHub stars count
|
// Fetch GitHub stars count
|
||||||
const fetchGitHubStars = useCallback(async () => {
|
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 {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://api.github.com/repos/9technologygroup/patchmon.net",
|
"https://api.github.com/repos/9technologygroup/patchmon.net",
|
||||||
@@ -248,6 +257,7 @@ const Layout = ({ children }) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setGithubStars(data.stargazers_count);
|
setGithubStars(data.stargazers_count);
|
||||||
|
localStorage.setItem("githubStarsFetchTime", now.toString());
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch GitHub stars:", error);
|
console.error("Failed to fetch GitHub stars:", error);
|
||||||
|
|||||||
29
frontend/src/constants/authPhases.js
Normal file
29
frontend/src/constants/authPhases.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { flushSync } from "react-dom";
|
||||||
|
import { AUTH_PHASES, isAuthPhase } from "../constants/authPhases";
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -20,11 +22,11 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [token, setToken] = useState(null);
|
const [token, setToken] = useState(null);
|
||||||
const [permissions, setPermissions] = useState(null);
|
const [permissions, setPermissions] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [permissionsLoading, setPermissionsLoading] = useState(false);
|
|
||||||
const [needsFirstTimeSetup, setNeedsFirstTimeSetup] = 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
|
// Define functions first
|
||||||
const fetchPermissions = useCallback(async (authToken) => {
|
const fetchPermissions = useCallback(async (authToken) => {
|
||||||
@@ -39,7 +41,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setPermissions(data);
|
setPermissions(data);
|
||||||
localStorage.setItem("permissions", JSON.stringify(data));
|
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch permissions");
|
console.error("Failed to fetch permissions");
|
||||||
@@ -65,36 +66,28 @@ export const AuthProvider = ({ children }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedToken = localStorage.getItem("token");
|
const storedToken = localStorage.getItem("token");
|
||||||
const storedUser = localStorage.getItem("user");
|
const storedUser = localStorage.getItem("user");
|
||||||
const storedPermissions = localStorage.getItem("permissions");
|
|
||||||
|
|
||||||
if (storedToken && storedUser) {
|
if (storedToken && storedUser) {
|
||||||
try {
|
try {
|
||||||
setToken(storedToken);
|
setToken(storedToken);
|
||||||
setUser(JSON.parse(storedUser));
|
setUser(JSON.parse(storedUser));
|
||||||
if (storedPermissions) {
|
// Fetch permissions from backend
|
||||||
setPermissions(JSON.parse(storedPermissions));
|
fetchPermissions(storedToken);
|
||||||
} else {
|
// User is authenticated, skip setup check
|
||||||
// Use the proper fetchPermissions function
|
setAuthPhase(AUTH_PHASES.READY);
|
||||||
fetchPermissions(storedToken);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing stored user data:", error);
|
console.error("Error parsing stored user data:", error);
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem("user");
|
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]);
|
}, [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) => {
|
const login = async (username, password) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/v1/auth/login", {
|
const response = await fetch("/api/v1/auth/login", {
|
||||||
@@ -108,6 +101,12 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
// Check if TFA is required
|
||||||
|
if (data.requiresTfa) {
|
||||||
|
return { success: true, requiresTfa: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular successful login
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
localStorage.setItem("token", data.token);
|
localStorage.setItem("token", data.token);
|
||||||
@@ -147,7 +146,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
setPermissions(null);
|
setPermissions(null);
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem("user");
|
||||||
localStorage.removeItem("permissions");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,10 +200,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAuthenticated = () => {
|
|
||||||
return !!(token && user);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAdmin = () => {
|
const isAdmin = () => {
|
||||||
return user?.role === "admin";
|
return user?.role === "admin";
|
||||||
};
|
};
|
||||||
@@ -243,42 +237,59 @@ export const AuthProvider = ({ children }) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setNeedsFirstTimeSetup(!data.hasAdminUsers);
|
setNeedsFirstTimeSetup(!data.hasAdminUsers);
|
||||||
|
setAuthPhase(AUTH_PHASES.READY); // Setup check complete, move to ready phase
|
||||||
} else {
|
} else {
|
||||||
// If endpoint doesn't exist or fails, assume setup is needed
|
// If endpoint doesn't exist or fails, assume setup is needed
|
||||||
setNeedsFirstTimeSetup(true);
|
setNeedsFirstTimeSetup(true);
|
||||||
|
setAuthPhase(AUTH_PHASES.READY);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking admin users:", error);
|
console.error("Error checking admin users:", error);
|
||||||
// If there's an error, assume setup is needed
|
// If there's an error, assume setup is needed
|
||||||
setNeedsFirstTimeSetup(true);
|
setNeedsFirstTimeSetup(true);
|
||||||
} finally {
|
setAuthPhase(AUTH_PHASES.READY);
|
||||||
setCheckingSetup(false);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Check for admin users on initial load
|
// Check for admin users ONLY when in CHECKING_SETUP phase
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token && !user) {
|
if (isAuthPhase.checkingSetup(authPhase)) {
|
||||||
checkAdminUsersExist();
|
checkAdminUsersExist();
|
||||||
} else {
|
|
||||||
setCheckingSetup(false);
|
|
||||||
}
|
}
|
||||||
}, [token, user, checkAdminUsersExist]);
|
}, [authPhase, checkAdminUsersExist]);
|
||||||
|
|
||||||
const setAuthState = (authToken, authUser) => {
|
const setAuthState = (authToken, authUser) => {
|
||||||
setToken(authToken);
|
// Use flushSync to ensure all state updates are applied synchronously
|
||||||
setUser(authUser);
|
flushSync(() => {
|
||||||
|
setToken(authToken);
|
||||||
|
setUser(authUser);
|
||||||
|
setNeedsFirstTimeSetup(false);
|
||||||
|
setAuthPhase(AUTH_PHASES.READY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store in localStorage after state is updated
|
||||||
localStorage.setItem("token", authToken);
|
localStorage.setItem("token", authToken);
|
||||||
localStorage.setItem("user", JSON.stringify(authUser));
|
localStorage.setItem("user", JSON.stringify(authUser));
|
||||||
|
|
||||||
|
// Fetch permissions immediately for the new authenticated user
|
||||||
|
fetchPermissions(authToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 = {
|
const value = {
|
||||||
user,
|
user,
|
||||||
token,
|
token,
|
||||||
permissions,
|
permissions,
|
||||||
isLoading: isLoading || permissionsLoading || checkingSetup,
|
isLoading,
|
||||||
needsFirstTimeSetup,
|
needsFirstTimeSetup,
|
||||||
checkingSetup,
|
authPhase,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
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 { settingsAPI, versionAPI } from "../utils/api";
|
||||||
import { useAuth } from "./AuthContext";
|
import { useAuth } from "./AuthContext";
|
||||||
|
|
||||||
@@ -17,16 +18,26 @@ export const useUpdateNotification = () => {
|
|||||||
|
|
||||||
export const UpdateNotificationProvider = ({ children }) => {
|
export const UpdateNotificationProvider = ({ children }) => {
|
||||||
const [dismissed, setDismissed] = useState(false);
|
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({
|
const { data: settings, isLoading: settingsLoading } = useQuery({
|
||||||
queryKey: ["settings"],
|
queryKey: ["settings"],
|
||||||
queryFn: () => settingsAPI.get().then((res) => res.data),
|
queryFn: () => settingsAPI.get().then((res) => res.data),
|
||||||
enabled: !!(user && token),
|
staleTime: 5 * 60 * 1000, // Settings stay fresh for 5 minutes
|
||||||
retry: 1,
|
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
|
// Query for update information
|
||||||
const {
|
const {
|
||||||
data: updateData,
|
data: updateData,
|
||||||
@@ -38,7 +49,7 @@ export const UpdateNotificationProvider = ({ children }) => {
|
|||||||
staleTime: 10 * 60 * 1000, // Data stays fresh for 10 minutes
|
staleTime: 10 * 60 * 1000, // Data stays fresh for 10 minutes
|
||||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||||
retry: 1,
|
retry: 1,
|
||||||
enabled: !!(user && token && settings && !settingsLoading), // Only run when authenticated and settings are loaded
|
enabled: isQueryEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateAvailable = updateData?.isUpdateAvailable && !dismissed;
|
const updateAvailable = updateData?.isUpdateAvailable && !dismissed;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import React from "react";
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import App from "./App.jsx";
|
import App from "./App.jsx";
|
||||||
@@ -17,11 +17,11 @@ const queryClient = new QueryClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<App />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,11 +44,7 @@ const HostDetail = () => {
|
|||||||
const [showCredentialsModal, setShowCredentialsModal] = useState(false);
|
const [showCredentialsModal, setShowCredentialsModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [showAllUpdates, setShowAllUpdates] = useState(false);
|
const [showAllUpdates, setShowAllUpdates] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState(() => {
|
const [activeTab, setActiveTab] = useState("host");
|
||||||
// Restore tab state from localStorage
|
|
||||||
const savedTab = localStorage.getItem(`host-detail-tab-${hostId}`);
|
|
||||||
return savedTab || "host";
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: host,
|
data: host,
|
||||||
@@ -63,10 +59,9 @@ const HostDetail = () => {
|
|||||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save tab state to localStorage when it changes
|
// Tab change handler
|
||||||
const handleTabChange = (tabName) => {
|
const handleTabChange = (tabName) => {
|
||||||
setActiveTab(tabName);
|
setActiveTab(tabName);
|
||||||
localStorage.setItem(`host-detail-tab-${hostId}`, tabName);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-show credentials modal for new/pending hosts
|
// Auto-show credentials modal for new/pending hosts
|
||||||
@@ -84,7 +79,7 @@ const HostDetail = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle auto-update mutation
|
// Toggle agent auto-update mutation (updates PatchMon agent script, not system packages)
|
||||||
const toggleAutoUpdateMutation = useMutation({
|
const toggleAutoUpdateMutation = useMutation({
|
||||||
mutationFn: (auto_update) =>
|
mutationFn: (auto_update) =>
|
||||||
adminHostsAPI
|
adminHostsAPI
|
||||||
@@ -350,7 +345,7 @@ const HostDetail = () => {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5">
|
||||||
Friendly Name
|
Friendly Name
|
||||||
</p>
|
</p>
|
||||||
<InlineEdit
|
<InlineEdit
|
||||||
@@ -374,7 +369,7 @@ const HostDetail = () => {
|
|||||||
|
|
||||||
{host.hostname && (
|
{host.hostname && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5">
|
||||||
System Hostname
|
System Hostname
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">
|
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">
|
||||||
@@ -384,7 +379,7 @@ const HostDetail = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5">
|
||||||
Host Group
|
Host Group
|
||||||
</p>
|
</p>
|
||||||
{host.host_groups ? (
|
{host.host_groups ? (
|
||||||
@@ -402,7 +397,7 @@ const HostDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5">
|
||||||
Operating System
|
Operating System
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -413,43 +408,38 @@ const HostDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{host.agent_version && (
|
<div>
|
||||||
<div className="flex items-center justify-between">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5">
|
||||||
<div>
|
Agent Version
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
</p>
|
||||||
Agent Version
|
<p className="font-medium text-secondary-900 dark:text-white text-sm">
|
||||||
</p>
|
{host.agent_version || "Unknown"}
|
||||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">
|
</p>
|
||||||
{host.agent_version}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5">
|
||||||
<span className="text-xs text-secondary-500 dark:text-secondary-300">
|
Auto-update
|
||||||
Auto-update
|
</p>
|
||||||
</span>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() =>
|
||||||
onClick={() =>
|
toggleAutoUpdateMutation.mutate(!host.auto_update)
|
||||||
toggleAutoUpdateMutation.mutate(!host.auto_update)
|
}
|
||||||
}
|
disabled={toggleAutoUpdateMutation.isPending}
|
||||||
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 ${
|
||||||
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
|
||||||
host.auto_update
|
? "bg-primary-600 dark:bg-primary-500"
|
||||||
? "bg-primary-600 dark:bg-primary-500"
|
: "bg-secondary-200 dark:bg-secondary-600"
|
||||||
: "bg-secondary-200 dark:bg-secondary-600"
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
<span
|
||||||
<span
|
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
|
||||||
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
|
host.auto_update ? "translate-x-5" : "translate-x-1"
|
||||||
host.auto_update
|
}`}
|
||||||
? "translate-x-5"
|
/>
|
||||||
: "translate-x-1"
|
</button>
|
||||||
}`}
|
</div>
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -326,7 +326,12 @@ const Hosts = () => {
|
|||||||
{ id: "os", label: "OS", visible: true, order: 4 },
|
{ id: "os", label: "OS", visible: true, order: 4 },
|
||||||
{ id: "os_version", label: "OS Version", visible: false, order: 5 },
|
{ id: "os_version", label: "OS Version", visible: false, order: 5 },
|
||||||
{ id: "agent_version", label: "Agent Version", visible: true, order: 6 },
|
{ 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: "status", label: "Status", visible: true, order: 8 },
|
||||||
{ id: "updates", label: "Updates", visible: true, order: 9 },
|
{ id: "updates", label: "Updates", visible: true, order: 9 },
|
||||||
{ id: "last_update", label: "Last Update", visible: true, order: 10 },
|
{ id: "last_update", label: "Last Update", visible: true, order: 10 },
|
||||||
|
|||||||
@@ -67,23 +67,17 @@ const Login = () => {
|
|||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authAPI.login(
|
// Use the AuthContext login function which handles everything
|
||||||
formData.username,
|
const result = await login(formData.username, formData.password);
|
||||||
formData.password,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.requiresTfa) {
|
if (result.requiresTfa) {
|
||||||
setRequiresTfa(true);
|
setRequiresTfa(true);
|
||||||
setTfaUsername(formData.username);
|
setTfaUsername(formData.username);
|
||||||
setError("");
|
setError("");
|
||||||
|
} else if (result.success) {
|
||||||
|
navigate("/");
|
||||||
} else {
|
} else {
|
||||||
// Regular login successful
|
setError(result.error || "Login failed");
|
||||||
const result = await login(formData.username, formData.password);
|
|
||||||
if (result.success) {
|
|
||||||
navigate("/");
|
|
||||||
} else {
|
|
||||||
setError(result.error || "Login failed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.error || "Login failed");
|
setError(err.response?.data?.error || "Login failed");
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ api.interceptors.response.use(
|
|||||||
// Handle unauthorized
|
// Handle unauthorized
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem("user");
|
||||||
localStorage.removeItem("permissions");
|
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@biomejs/biome": "2.2.4",
|
"@biomejs/biome": "2.2.4",
|
||||||
"@commitlint/cli": "^20.0.0",
|
"@commitlint/cli": "^20.0.0",
|
||||||
"@commitlint/config-conventional": "^20.0.0",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"lefthook": "^1.13.4",
|
"lefthook": "^1.13.4",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^15.2.10",
|
||||||
@@ -1337,6 +1338,13 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@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": {
|
"node_modules/@types/conventional-commits-parser": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.4",
|
"@biomejs/biome": "2.2.4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@commitlint/cli": "^20.0.0",
|
"@commitlint/cli": "^20.0.0",
|
||||||
"@commitlint/config-conventional": "^20.0.0",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user