mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-04 05:53:27 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			renovate/e
			...
			post1-3-2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e57ff7612e | ||
| 
						 | 
					7a3d98862f | ||
| 
						 | 
					913976b7f6 | ||
| 
						 | 
					53ff3bb1e2 | 
@@ -860,6 +860,9 @@ router.post(
 | 
				
			|||||||
					last_login: user.last_login,
 | 
										last_login: user.last_login,
 | 
				
			||||||
					created_at: user.created_at,
 | 
										created_at: user.created_at,
 | 
				
			||||||
					updated_at: user.updated_at,
 | 
										updated_at: user.updated_at,
 | 
				
			||||||
 | 
										// Include user preferences so they're available immediately after login
 | 
				
			||||||
 | 
										theme_preference: user.theme_preference,
 | 
				
			||||||
 | 
										color_theme: user.color_theme,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error) {
 | 
				
			||||||
@@ -952,10 +955,24 @@ router.post(
 | 
				
			|||||||
				return res.status(401).json({ error: "Invalid verification code" });
 | 
									return res.status(401).json({ error: "Invalid verification code" });
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Update last login
 | 
								// Update last login and fetch complete user data
 | 
				
			||||||
			await prisma.users.update({
 | 
								const updatedUser = await prisma.users.update({
 | 
				
			||||||
				where: { id: user.id },
 | 
									where: { id: user.id },
 | 
				
			||||||
				data: { last_login: new Date() },
 | 
									data: { last_login: new Date() },
 | 
				
			||||||
 | 
									select: {
 | 
				
			||||||
 | 
										id: true,
 | 
				
			||||||
 | 
										username: true,
 | 
				
			||||||
 | 
										email: true,
 | 
				
			||||||
 | 
										first_name: true,
 | 
				
			||||||
 | 
										last_name: true,
 | 
				
			||||||
 | 
										role: true,
 | 
				
			||||||
 | 
										is_active: true,
 | 
				
			||||||
 | 
										last_login: true,
 | 
				
			||||||
 | 
										created_at: true,
 | 
				
			||||||
 | 
										updated_at: true,
 | 
				
			||||||
 | 
										theme_preference: true,
 | 
				
			||||||
 | 
										color_theme: true,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Create session with access and refresh tokens
 | 
								// Create session with access and refresh tokens
 | 
				
			||||||
@@ -975,14 +992,7 @@ router.post(
 | 
				
			|||||||
				refresh_token: session.refresh_token,
 | 
									refresh_token: session.refresh_token,
 | 
				
			||||||
				expires_at: session.expires_at,
 | 
									expires_at: session.expires_at,
 | 
				
			||||||
				tfa_bypass_until: session.tfa_bypass_until,
 | 
									tfa_bypass_until: session.tfa_bypass_until,
 | 
				
			||||||
				user: {
 | 
									user: updatedUser,
 | 
				
			||||||
					id: user.id,
 | 
					 | 
				
			||||||
					username: user.username,
 | 
					 | 
				
			||||||
					email: user.email,
 | 
					 | 
				
			||||||
					first_name: user.first_name,
 | 
					 | 
				
			||||||
					last_name: user.last_name,
 | 
					 | 
				
			||||||
					role: user.role,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error) {
 | 
				
			||||||
			console.error("TFA verification error:", error);
 | 
								console.error("TFA verification error:", error);
 | 
				
			||||||
@@ -1014,13 +1024,27 @@ router.put(
 | 
				
			|||||||
			.withMessage("Username must be at least 3 characters"),
 | 
								.withMessage("Username must be at least 3 characters"),
 | 
				
			||||||
		body("email").optional().isEmail().withMessage("Valid email is required"),
 | 
							body("email").optional().isEmail().withMessage("Valid email is required"),
 | 
				
			||||||
		body("first_name")
 | 
							body("first_name")
 | 
				
			||||||
			.optional()
 | 
								.optional({ nullable: true, checkFalsy: true })
 | 
				
			||||||
			.isLength({ min: 1 })
 | 
								.custom((value) => {
 | 
				
			||||||
			.withMessage("First name must be at least 1 character"),
 | 
									// Allow null, undefined, or empty string to clear the field
 | 
				
			||||||
 | 
									if (value === null || value === undefined || value === "") {
 | 
				
			||||||
 | 
										return true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									// If provided, must be at least 1 character after trimming
 | 
				
			||||||
 | 
									return typeof value === "string" && value.trim().length >= 1;
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.withMessage("First name must be at least 1 character if provided"),
 | 
				
			||||||
		body("last_name")
 | 
							body("last_name")
 | 
				
			||||||
			.optional()
 | 
								.optional({ nullable: true, checkFalsy: true })
 | 
				
			||||||
			.isLength({ min: 1 })
 | 
								.custom((value) => {
 | 
				
			||||||
			.withMessage("Last name must be at least 1 character"),
 | 
									// Allow null, undefined, or empty string to clear the field
 | 
				
			||||||
 | 
									if (value === null || value === undefined || value === "") {
 | 
				
			||||||
 | 
										return true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									// If provided, must be at least 1 character after trimming
 | 
				
			||||||
 | 
									return typeof value === "string" && value.trim().length >= 1;
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.withMessage("Last name must be at least 1 character if provided"),
 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
	async (req, res) => {
 | 
						async (req, res) => {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
@@ -1034,16 +1058,22 @@ router.put(
 | 
				
			|||||||
				updated_at: new Date(),
 | 
									updated_at: new Date(),
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (username) updateData.username = username;
 | 
								// Handle all fields consistently - trim and update if provided
 | 
				
			||||||
			if (email) updateData.email = email;
 | 
								if (username) updateData.username = username.trim();
 | 
				
			||||||
			// Handle first_name and last_name - allow empty strings to clear the field
 | 
								if (email) updateData.email = email.trim();
 | 
				
			||||||
			if (first_name !== undefined) {
 | 
								if (first_name !== undefined) {
 | 
				
			||||||
 | 
									// Allow null or empty string to clear the field, otherwise trim
 | 
				
			||||||
				updateData.first_name =
 | 
									updateData.first_name =
 | 
				
			||||||
					first_name === "" ? null : first_name.trim() || null;
 | 
										first_name === "" || first_name === null
 | 
				
			||||||
 | 
											? null
 | 
				
			||||||
 | 
											: first_name.trim() || null;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (last_name !== undefined) {
 | 
								if (last_name !== undefined) {
 | 
				
			||||||
 | 
									// Allow null or empty string to clear the field, otherwise trim
 | 
				
			||||||
				updateData.last_name =
 | 
									updateData.last_name =
 | 
				
			||||||
					last_name === "" ? null : last_name.trim() || null;
 | 
										last_name === "" || last_name === null
 | 
				
			||||||
 | 
											? null
 | 
				
			||||||
 | 
											: last_name.trim() || null;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Check if username/email already exists (excluding current user)
 | 
								// Check if username/email already exists (excluding current user)
 | 
				
			||||||
@@ -1106,16 +1136,6 @@ router.put(
 | 
				
			|||||||
			// Use fresh data if available, otherwise fallback to updatedUser
 | 
								// Use fresh data if available, otherwise fallback to updatedUser
 | 
				
			||||||
			const responseUser = freshUser || updatedUser;
 | 
								const responseUser = freshUser || updatedUser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Log update for debugging (only log in non-production)
 | 
					 | 
				
			||||||
			if (process.env.NODE_ENV !== "production") {
 | 
					 | 
				
			||||||
				console.log("Profile updated:", {
 | 
					 | 
				
			||||||
					userId: req.user.id,
 | 
					 | 
				
			||||||
					first_name: responseUser.first_name,
 | 
					 | 
				
			||||||
					last_name: responseUser.last_name,
 | 
					 | 
				
			||||||
					updated_at: responseUser.updated_at,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			res.json({
 | 
								res.json({
 | 
				
			||||||
				message: "Profile updated successfully",
 | 
									message: "Profile updated successfully",
 | 
				
			||||||
				user: responseUser,
 | 
									user: responseUser,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -450,8 +450,8 @@ function AppRoutes() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function App() {
 | 
					function App() {
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<ThemeProvider>
 | 
					 | 
				
			||||||
		<AuthProvider>
 | 
							<AuthProvider>
 | 
				
			||||||
 | 
								<ThemeProvider>
 | 
				
			||||||
				<SettingsProvider>
 | 
									<SettingsProvider>
 | 
				
			||||||
					<ColorThemeProvider>
 | 
										<ColorThemeProvider>
 | 
				
			||||||
						<UpdateNotificationProvider>
 | 
											<UpdateNotificationProvider>
 | 
				
			||||||
@@ -461,8 +461,8 @@ function App() {
 | 
				
			|||||||
						</UpdateNotificationProvider>
 | 
											</UpdateNotificationProvider>
 | 
				
			||||||
					</ColorThemeProvider>
 | 
										</ColorThemeProvider>
 | 
				
			||||||
				</SettingsProvider>
 | 
									</SettingsProvider>
 | 
				
			||||||
			</AuthProvider>
 | 
					 | 
				
			||||||
			</ThemeProvider>
 | 
								</ThemeProvider>
 | 
				
			||||||
 | 
							</AuthProvider>
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,6 +138,9 @@ export const AuthProvider = ({ children }) => {
 | 
				
			|||||||
					setPermissions(userPermissions);
 | 
										setPermissions(userPermissions);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Note: User preferences will be automatically fetched by ColorThemeContext
 | 
				
			||||||
 | 
									// when the component mounts, so no need to invalidate here
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return { success: true };
 | 
									return { success: true };
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				// Handle HTTP error responses (like 500 CORS errors)
 | 
									// Handle HTTP error responses (like 500 CORS errors)
 | 
				
			||||||
@@ -224,8 +227,6 @@ export const AuthProvider = ({ children }) => {
 | 
				
			|||||||
			const data = await response.json();
 | 
								const data = await response.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (response.ok) {
 | 
								if (response.ok) {
 | 
				
			||||||
				console.log("Profile updated - received user data:", data.user);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Validate that we received user data with expected fields
 | 
									// Validate that we received user data with expected fields
 | 
				
			||||||
				if (!data.user || !data.user.id) {
 | 
									if (!data.user || !data.user.id) {
 | 
				
			||||||
					console.error("Invalid user data in response:", data);
 | 
										console.error("Invalid user data in response:", data);
 | 
				
			||||||
@@ -239,15 +240,6 @@ export const AuthProvider = ({ children }) => {
 | 
				
			|||||||
				setUser(data.user);
 | 
									setUser(data.user);
 | 
				
			||||||
				localStorage.setItem("user", JSON.stringify(data.user));
 | 
									localStorage.setItem("user", JSON.stringify(data.user));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// Log update for debugging (only in non-production)
 | 
					 | 
				
			||||||
				if (process.env.NODE_ENV !== "production") {
 | 
					 | 
				
			||||||
					console.log("User data updated in localStorage:", {
 | 
					 | 
				
			||||||
						id: data.user.id,
 | 
					 | 
				
			||||||
						first_name: data.user.first_name,
 | 
					 | 
				
			||||||
						last_name: data.user.last_name,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				return { success: true, user: data.user };
 | 
									return { success: true, user: data.user };
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				// Handle HTTP error responses (like 500 CORS errors)
 | 
									// Handle HTTP error responses (like 500 CORS errors)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,15 @@
 | 
				
			|||||||
import { useQuery } from "@tanstack/react-query";
 | 
					import { useQuery, useQueryClient } from "@tanstack/react-query";
 | 
				
			||||||
import { createContext, useContext, useEffect, useState } from "react";
 | 
					import {
 | 
				
			||||||
 | 
						createContext,
 | 
				
			||||||
 | 
						useCallback,
 | 
				
			||||||
 | 
						useContext,
 | 
				
			||||||
 | 
						useEffect,
 | 
				
			||||||
 | 
						useMemo,
 | 
				
			||||||
 | 
						useRef,
 | 
				
			||||||
 | 
						useState,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
import { userPreferencesAPI } from "../utils/api";
 | 
					import { userPreferencesAPI } from "../utils/api";
 | 
				
			||||||
 | 
					import { useAuth } from "./AuthContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ColorThemeContext = createContext();
 | 
					const ColorThemeContext = createContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -123,48 +132,108 @@ export const THEME_PRESETS = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ColorThemeProvider = ({ children }) => {
 | 
					export const ColorThemeProvider = ({ children }) => {
 | 
				
			||||||
	const [colorTheme, setColorTheme] = useState(() => {
 | 
						const queryClient = useQueryClient();
 | 
				
			||||||
		// Initialize from localStorage for immediate render
 | 
						const lastThemeRef = useRef(null);
 | 
				
			||||||
		return localStorage.getItem("colorTheme") || "cyber_blue";
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	const [isLoading, setIsLoading] = useState(true);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fetch user preferences from backend
 | 
						// Use reactive authentication state from AuthContext
 | 
				
			||||||
	const { data: userPreferences } = useQuery({
 | 
						// This ensures the query re-enables when user logs in
 | 
				
			||||||
 | 
						const { user } = useAuth();
 | 
				
			||||||
 | 
						const isAuthenticated = !!user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Source of truth: Database (via userPreferences query)
 | 
				
			||||||
 | 
						// localStorage is only used as a temporary cache until DB loads
 | 
				
			||||||
 | 
						// Only fetch if user is authenticated to avoid 401 errors on login page
 | 
				
			||||||
 | 
						const { data: userPreferences, isLoading: preferencesLoading } = useQuery({
 | 
				
			||||||
		queryKey: ["userPreferences"],
 | 
							queryKey: ["userPreferences"],
 | 
				
			||||||
		queryFn: () => userPreferencesAPI.get().then((res) => res.data),
 | 
							queryFn: () => userPreferencesAPI.get().then((res) => res.data),
 | 
				
			||||||
		retry: 1,
 | 
							enabled: isAuthenticated, // Only run query if user is authenticated
 | 
				
			||||||
 | 
							retry: 2,
 | 
				
			||||||
		staleTime: 5 * 60 * 1000, // 5 minutes
 | 
							staleTime: 5 * 60 * 1000, // 5 minutes
 | 
				
			||||||
 | 
							refetchOnWindowFocus: true, // Refetch when user returns to tab
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Update theme when preferences are loaded
 | 
						// Get theme from database (source of truth), fallback to user object from login, then localStorage cache, then default
 | 
				
			||||||
 | 
						// Memoize to prevent recalculation on every render
 | 
				
			||||||
 | 
						const colorThemeValue = useMemo(() => {
 | 
				
			||||||
 | 
							return (
 | 
				
			||||||
 | 
								userPreferences?.color_theme ||
 | 
				
			||||||
 | 
								user?.color_theme ||
 | 
				
			||||||
 | 
								localStorage.getItem("colorTheme") ||
 | 
				
			||||||
 | 
								"cyber_blue"
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}, [userPreferences?.color_theme, user?.color_theme]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Only update state if the theme value actually changed (prevent loops)
 | 
				
			||||||
 | 
						const [colorTheme, setColorTheme] = useState(() => colorThemeValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						useEffect(() => {
 | 
				
			||||||
 | 
							// Only update if the value actually changed from what we last saw (prevent loops)
 | 
				
			||||||
 | 
							if (colorThemeValue !== lastThemeRef.current) {
 | 
				
			||||||
 | 
								setColorTheme(colorThemeValue);
 | 
				
			||||||
 | 
								lastThemeRef.current = colorThemeValue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, [colorThemeValue]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const isLoading = preferencesLoading;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Sync localStorage cache when DB data is available (for offline/performance)
 | 
				
			||||||
	useEffect(() => {
 | 
						useEffect(() => {
 | 
				
			||||||
		if (userPreferences?.color_theme) {
 | 
							if (userPreferences?.color_theme) {
 | 
				
			||||||
			setColorTheme(userPreferences.color_theme);
 | 
					 | 
				
			||||||
			localStorage.setItem("colorTheme", userPreferences.color_theme);
 | 
								localStorage.setItem("colorTheme", userPreferences.color_theme);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		setIsLoading(false);
 | 
						}, [userPreferences?.color_theme]);
 | 
				
			||||||
	}, [userPreferences]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const updateColorTheme = async (theme) => {
 | 
						const updateColorTheme = useCallback(
 | 
				
			||||||
 | 
							async (theme) => {
 | 
				
			||||||
 | 
								// Store previous theme for potential revert
 | 
				
			||||||
 | 
								const previousTheme = colorTheme;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Immediately update state for instant UI feedback
 | 
				
			||||||
			setColorTheme(theme);
 | 
								setColorTheme(theme);
 | 
				
			||||||
 | 
								lastThemeRef.current = theme;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Also update localStorage cache
 | 
				
			||||||
			localStorage.setItem("colorTheme", theme);
 | 
								localStorage.setItem("colorTheme", theme);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Save to backend
 | 
								// Save to backend (source of truth)
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				await userPreferencesAPI.update({ color_theme: theme });
 | 
									await userPreferencesAPI.update({ color_theme: theme });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Invalidate and refetch user preferences to ensure sync across tabs/browsers
 | 
				
			||||||
 | 
									await queryClient.invalidateQueries({ queryKey: ["userPreferences"] });
 | 
				
			||||||
			} catch (error) {
 | 
								} catch (error) {
 | 
				
			||||||
				console.error("Failed to save color theme preference:", error);
 | 
									console.error("Failed to save color theme preference:", error);
 | 
				
			||||||
			// Theme is already set locally, so user still sees the change
 | 
									// Revert to previous theme if save failed
 | 
				
			||||||
		}
 | 
									setColorTheme(previousTheme);
 | 
				
			||||||
	};
 | 
									lastThemeRef.current = previousTheme;
 | 
				
			||||||
 | 
									localStorage.setItem("colorTheme", previousTheme);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const value = {
 | 
									// Invalidate to refresh from DB
 | 
				
			||||||
 | 
									await queryClient.invalidateQueries({ queryKey: ["userPreferences"] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Show error to user if possible (could add toast notification here)
 | 
				
			||||||
 | 
									throw error; // Re-throw so calling code can handle it
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							[colorTheme, queryClient],
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Memoize themeConfig to prevent unnecessary re-renders
 | 
				
			||||||
 | 
						const themeConfig = useMemo(
 | 
				
			||||||
 | 
							() => THEME_PRESETS[colorTheme] || THEME_PRESETS.default,
 | 
				
			||||||
 | 
							[colorTheme],
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Memoize the context value to prevent unnecessary re-renders
 | 
				
			||||||
 | 
						const value = useMemo(
 | 
				
			||||||
 | 
							() => ({
 | 
				
			||||||
			colorTheme,
 | 
								colorTheme,
 | 
				
			||||||
			setColorTheme: updateColorTheme,
 | 
								setColorTheme: updateColorTheme,
 | 
				
			||||||
		themeConfig: THEME_PRESETS[colorTheme] || THEME_PRESETS.default,
 | 
								themeConfig,
 | 
				
			||||||
			isLoading,
 | 
								isLoading,
 | 
				
			||||||
	};
 | 
							}),
 | 
				
			||||||
 | 
							[colorTheme, themeConfig, isLoading, updateColorTheme],
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<ColorThemeContext.Provider value={value}>
 | 
							<ColorThemeContext.Provider value={value}>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import { useQuery } from "@tanstack/react-query";
 | 
					import { useQuery } from "@tanstack/react-query";
 | 
				
			||||||
import { createContext, useContext, useEffect, useState } from "react";
 | 
					import { createContext, useContext, useEffect, useState } from "react";
 | 
				
			||||||
import { userPreferencesAPI } from "../utils/api";
 | 
					import { userPreferencesAPI } from "../utils/api";
 | 
				
			||||||
 | 
					import { useAuth } from "./AuthContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ThemeContext = createContext();
 | 
					const ThemeContext = createContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,21 +27,29 @@ export const ThemeProvider = ({ children }) => {
 | 
				
			|||||||
		return "light";
 | 
							return "light";
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fetch user preferences from backend
 | 
						// Use reactive authentication state from AuthContext
 | 
				
			||||||
 | 
						// This ensures the query re-enables when user logs in
 | 
				
			||||||
 | 
						const { user } = useAuth();
 | 
				
			||||||
 | 
						const isAuthenticated = !!user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fetch user preferences from backend (only if authenticated)
 | 
				
			||||||
	const { data: userPreferences } = useQuery({
 | 
						const { data: userPreferences } = useQuery({
 | 
				
			||||||
		queryKey: ["userPreferences"],
 | 
							queryKey: ["userPreferences"],
 | 
				
			||||||
		queryFn: () => userPreferencesAPI.get().then((res) => res.data),
 | 
							queryFn: () => userPreferencesAPI.get().then((res) => res.data),
 | 
				
			||||||
 | 
							enabled: isAuthenticated, // Only run query if user is authenticated
 | 
				
			||||||
		retry: 1,
 | 
							retry: 1,
 | 
				
			||||||
		staleTime: 5 * 60 * 1000, // 5 minutes
 | 
							staleTime: 5 * 60 * 1000, // 5 minutes
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Sync with user preferences from backend
 | 
						// Sync with user preferences from backend or user object from login
 | 
				
			||||||
	useEffect(() => {
 | 
						useEffect(() => {
 | 
				
			||||||
		if (userPreferences?.theme_preference) {
 | 
							const preferredTheme =
 | 
				
			||||||
			setTheme(userPreferences.theme_preference);
 | 
								userPreferences?.theme_preference || user?.theme_preference;
 | 
				
			||||||
			localStorage.setItem("theme", userPreferences.theme_preference);
 | 
							if (preferredTheme) {
 | 
				
			||||||
 | 
								setTheme(preferredTheme);
 | 
				
			||||||
 | 
								localStorage.setItem("theme", preferredTheme);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}, [userPreferences]);
 | 
						}, [userPreferences, user?.theme_preference]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	useEffect(() => {
 | 
						useEffect(() => {
 | 
				
			||||||
		// Apply theme to document
 | 
							// Apply theme to document
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -248,7 +248,8 @@ const Login = () => {
 | 
				
			|||||||
			} catch (error) {
 | 
								} catch (error) {
 | 
				
			||||||
				console.error("Failed to fetch GitHub data:", error);
 | 
									console.error("Failed to fetch GitHub data:", error);
 | 
				
			||||||
				// Set fallback data if nothing cached
 | 
									// Set fallback data if nothing cached
 | 
				
			||||||
				if (!latestRelease) {
 | 
									const cachedRelease = localStorage.getItem("githubLatestRelease");
 | 
				
			||||||
 | 
									if (!cachedRelease) {
 | 
				
			||||||
					setLatestRelease({
 | 
										setLatestRelease({
 | 
				
			||||||
						version: "v1.3.0",
 | 
											version: "v1.3.0",
 | 
				
			||||||
						name: "Latest Release",
 | 
											name: "Latest Release",
 | 
				
			||||||
@@ -260,7 +261,7 @@ const Login = () => {
 | 
				
			|||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fetchGitHubData();
 | 
							fetchGitHubData();
 | 
				
			||||||
	}, [latestRelease]);
 | 
						}, []); // Run once on mount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const handleSubmit = async (e) => {
 | 
						const handleSubmit = async (e) => {
 | 
				
			||||||
		e.preventDefault();
 | 
							e.preventDefault();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										44
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								setup.sh
									
									
									
									
									
								
							@@ -66,27 +66,27 @@ SELECTED_SERVICE_NAME=""
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Functions
 | 
					# Functions
 | 
				
			||||||
print_status() {
 | 
					print_status() {
 | 
				
			||||||
    echo -e "${GREEN}✅ $1${NC}"
 | 
					    printf "${GREEN}%s${NC}\n" "$1"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
print_info() {
 | 
					print_info() {
 | 
				
			||||||
    echo -e "${BLUE}ℹ️  $1${NC}"
 | 
					    printf "${BLUE}%s${NC}\n" "$1"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
print_error() {
 | 
					print_error() {
 | 
				
			||||||
    echo -e "${RED}❌ $1${NC}"
 | 
					    printf "${RED}%s${NC}\n" "$1"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
print_warning() {
 | 
					print_warning() {
 | 
				
			||||||
    echo -e "${YELLOW}⚠️  $1${NC}"
 | 
					    printf "${YELLOW}%s${NC}\n" "$1"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
print_question() {
 | 
					print_question() {
 | 
				
			||||||
    echo -e "${BLUE}❓ $1${NC}"
 | 
					    printf "${BLUE}%s${NC}\n" "$1"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
print_success() {
 | 
					print_success() {
 | 
				
			||||||
    echo -e "${GREEN}🎉 $1${NC}"
 | 
					    printf "${GREEN}%s${NC}\n" "$1"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Interactive input functions
 | 
					# Interactive input functions
 | 
				
			||||||
@@ -1657,7 +1657,7 @@ start_services() {
 | 
				
			|||||||
        local logs=$(journalctl -u "$SERVICE_NAME" -n 50 --no-pager 2>/dev/null || echo "")
 | 
					        local logs=$(journalctl -u "$SERVICE_NAME" -n 50 --no-pager 2>/dev/null || echo "")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then
 | 
					        if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then
 | 
				
			||||||
            print_error "❌ Detected Redis authentication error!"
 | 
					            print_error "Detected Redis authentication error!"
 | 
				
			||||||
            print_info "The service cannot authenticate with Redis."
 | 
					            print_info "The service cannot authenticate with Redis."
 | 
				
			||||||
            echo ""
 | 
					            echo ""
 | 
				
			||||||
            print_info "Current Redis configuration in .env:"
 | 
					            print_info "Current Redis configuration in .env:"
 | 
				
			||||||
@@ -1681,18 +1681,18 @@ start_services() {
 | 
				
			|||||||
            print_info "     cat /etc/redis/users.acl"
 | 
					            print_info "     cat /etc/redis/users.acl"
 | 
				
			||||||
            echo ""
 | 
					            echo ""
 | 
				
			||||||
        elif echo "$logs" | grep -q "ECONNREFUSED.*postgresql\|Connection refused.*5432"; then
 | 
					        elif echo "$logs" | grep -q "ECONNREFUSED.*postgresql\|Connection refused.*5432"; then
 | 
				
			||||||
            print_error "❌ Detected PostgreSQL connection error!"
 | 
					            print_error "Detected PostgreSQL connection error!"
 | 
				
			||||||
            print_info "Check if PostgreSQL is running:"
 | 
					            print_info "Check if PostgreSQL is running:"
 | 
				
			||||||
            print_info "  systemctl status postgresql"
 | 
					            print_info "  systemctl status postgresql"
 | 
				
			||||||
        elif echo "$logs" | grep -q "ECONNREFUSED.*redis\|Connection refused.*6379"; then
 | 
					        elif echo "$logs" | grep -q "ECONNREFUSED.*redis\|Connection refused.*6379"; then
 | 
				
			||||||
            print_error "❌ Detected Redis connection error!"
 | 
					            print_error "Detected Redis connection error!"
 | 
				
			||||||
            print_info "Check if Redis is running:"
 | 
					            print_info "Check if Redis is running:"
 | 
				
			||||||
            print_info "  systemctl status redis-server"
 | 
					            print_info "  systemctl status redis-server"
 | 
				
			||||||
        elif echo "$logs" | grep -q "database.*does not exist"; then
 | 
					        elif echo "$logs" | grep -q "database.*does not exist"; then
 | 
				
			||||||
            print_error "❌ Database does not exist!"
 | 
					            print_error "Database does not exist!"
 | 
				
			||||||
            print_info "Database: $DB_NAME"
 | 
					            print_info "Database: $DB_NAME"
 | 
				
			||||||
        elif echo "$logs" | grep -q "Error:"; then
 | 
					        elif echo "$logs" | grep -q "Error:"; then
 | 
				
			||||||
            print_error "❌ Application error detected in logs"
 | 
					            print_error "Application error detected in logs"
 | 
				
			||||||
        fi
 | 
					        fi
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        echo ""
 | 
					        echo ""
 | 
				
			||||||
@@ -1741,9 +1741,9 @@ async function updateSettings() {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    console.log('✅ Database settings updated successfully');
 | 
					    console.log('Database settings updated successfully');
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error('❌ Error updating settings:', error.message);
 | 
					    console.error('Error updating settings:', error.message);
 | 
				
			||||||
    process.exit(1);
 | 
					    process.exit(1);
 | 
				
			||||||
  } finally {
 | 
					  } finally {
 | 
				
			||||||
    await prisma.\$disconnect();
 | 
					    await prisma.\$disconnect();
 | 
				
			||||||
@@ -1867,7 +1867,7 @@ EOF
 | 
				
			|||||||
    if [ -f "$SUMMARY_FILE" ]; then
 | 
					    if [ -f "$SUMMARY_FILE" ]; then
 | 
				
			||||||
        print_status "Deployment summary appended to: $SUMMARY_FILE"
 | 
					        print_status "Deployment summary appended to: $SUMMARY_FILE"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "⚠️  Failed to append to deployment-info.txt file"
 | 
					        print_error "Failed to append to deployment-info.txt file"
 | 
				
			||||||
        return 1
 | 
					        return 1
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1949,7 +1949,7 @@ EOF
 | 
				
			|||||||
        print_status "Deployment information saved to: $INFO_FILE"
 | 
					        print_status "Deployment information saved to: $INFO_FILE"
 | 
				
			||||||
        print_info "File details: $(ls -lh "$INFO_FILE" | awk '{print $5, $9}')"
 | 
					        print_info "File details: $(ls -lh "$INFO_FILE" | awk '{print $5, $9}')"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "⚠️  Failed to create deployment-info.txt file"
 | 
					        print_error "Failed to create deployment-info.txt file"
 | 
				
			||||||
        return 1
 | 
					        return 1
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -2142,7 +2142,7 @@ deploy_instance() {
 | 
				
			|||||||
    log_message "Backend port: $BACKEND_PORT"
 | 
					    log_message "Backend port: $BACKEND_PORT"
 | 
				
			||||||
    log_message "SSL enabled: $USE_LETSENCRYPT"
 | 
					    log_message "SSL enabled: $USE_LETSENCRYPT"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    print_status "🎉 PatchMon instance deployed successfully!"
 | 
					    print_status "PatchMon instance deployed successfully!"
 | 
				
			||||||
    echo ""
 | 
					    echo ""
 | 
				
			||||||
    print_info "Next steps:"
 | 
					    print_info "Next steps:"
 | 
				
			||||||
    echo "  • Visit your URL: $SERVER_PROTOCOL_SEL://$FQDN (ensure DNS is configured)"
 | 
					    echo "  • Visit your URL: $SERVER_PROTOCOL_SEL://$FQDN (ensure DNS is configured)"
 | 
				
			||||||
@@ -3236,7 +3236,7 @@ update_installation() {
 | 
				
			|||||||
    sleep 5
 | 
					    sleep 5
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if systemctl is-active --quiet "$service_name"; then
 | 
					    if systemctl is-active --quiet "$service_name"; then
 | 
				
			||||||
        print_success "✅ Update completed successfully!"
 | 
					        print_success "Update completed successfully!"
 | 
				
			||||||
        print_status "Service $service_name is running"
 | 
					        print_status "Service $service_name is running"
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Get new version
 | 
					        # Get new version
 | 
				
			||||||
@@ -3264,7 +3264,7 @@ update_installation() {
 | 
				
			|||||||
        local logs=$(journalctl -u "$service_name" -n 50 --no-pager 2>/dev/null || echo "")
 | 
					        local logs=$(journalctl -u "$service_name" -n 50 --no-pager 2>/dev/null || echo "")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then
 | 
					        if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then
 | 
				
			||||||
            print_error "❌ Detected Redis authentication error!"
 | 
					            print_error "Detected Redis authentication error!"
 | 
				
			||||||
            print_info "The service cannot authenticate with Redis."
 | 
					            print_info "The service cannot authenticate with Redis."
 | 
				
			||||||
            echo ""
 | 
					            echo ""
 | 
				
			||||||
            print_info "Current Redis configuration in .env:"
 | 
					            print_info "Current Redis configuration in .env:"
 | 
				
			||||||
@@ -3281,12 +3281,12 @@ update_installation() {
 | 
				
			|||||||
            print_info "     redis-cli --user $test_user --pass $test_pass -n ${test_db:-0} ping"
 | 
					            print_info "     redis-cli --user $test_user --pass $test_pass -n ${test_db:-0} ping"
 | 
				
			||||||
            echo ""
 | 
					            echo ""
 | 
				
			||||||
        elif echo "$logs" | grep -q "ECONNREFUSED"; then
 | 
					        elif echo "$logs" | grep -q "ECONNREFUSED"; then
 | 
				
			||||||
            print_error "❌ Detected connection refused error!"
 | 
					            print_error "Detected connection refused error!"
 | 
				
			||||||
            print_info "Check if required services are running:"
 | 
					            print_info "Check if required services are running:"
 | 
				
			||||||
            print_info "  systemctl status postgresql"
 | 
					            print_info "  systemctl status postgresql"
 | 
				
			||||||
            print_info "  systemctl status redis-server"
 | 
					            print_info "  systemctl status redis-server"
 | 
				
			||||||
        elif echo "$logs" | grep -q "Error:"; then
 | 
					        elif echo "$logs" | grep -q "Error:"; then
 | 
				
			||||||
            print_error "❌ Application error detected in logs"
 | 
					            print_error "Application error detected in logs"
 | 
				
			||||||
        fi
 | 
					        fi
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        echo ""
 | 
					        echo ""
 | 
				
			||||||
@@ -3319,7 +3319,7 @@ main() {
 | 
				
			|||||||
    # Handle update mode
 | 
					    # Handle update mode
 | 
				
			||||||
    if [ "$UPDATE_MODE" = "true" ]; then
 | 
					    if [ "$UPDATE_MODE" = "true" ]; then
 | 
				
			||||||
        print_banner
 | 
					        print_banner
 | 
				
			||||||
        print_info "🔄 PatchMon Update Mode"
 | 
					        print_info "PatchMon Update Mode"
 | 
				
			||||||
        echo ""
 | 
					        echo ""
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Select installation to update
 | 
					        # Select installation to update
 | 
				
			||||||
@@ -3335,7 +3335,7 @@ main() {
 | 
				
			|||||||
    # Check if existing installations are present
 | 
					    # Check if existing installations are present
 | 
				
			||||||
    local existing_installs=($(detect_installations))
 | 
					    local existing_installs=($(detect_installations))
 | 
				
			||||||
    if [ ${#existing_installs[@]} -gt 0 ]; then
 | 
					    if [ ${#existing_installs[@]} -gt 0 ]; then
 | 
				
			||||||
        print_warning "⚠️  Found ${#existing_installs[@]} existing PatchMon installation(s):"
 | 
					        print_warning "Found ${#existing_installs[@]} existing PatchMon installation(s):"
 | 
				
			||||||
        for install in "${existing_installs[@]}"; do
 | 
					        for install in "${existing_installs[@]}"; do
 | 
				
			||||||
            print_info "   - $install"
 | 
					            print_info "   - $install"
 | 
				
			||||||
        done
 | 
					        done
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user