1.3.2 final

fixed theme and user profile settings
This commit is contained in:
9 Technology Group LTD
2025-10-31 22:25:22 +00:00
committed by GitHub
6 changed files with 176 additions and 85 deletions

View File

@@ -860,6 +860,9 @@ router.post(
last_login: user.last_login,
created_at: user.created_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) {
@@ -952,10 +955,24 @@ router.post(
return res.status(401).json({ error: "Invalid verification code" });
}
// Update last login
await prisma.users.update({
// Update last login and fetch complete user data
const updatedUser = await prisma.users.update({
where: { id: user.id },
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
@@ -975,14 +992,7 @@ router.post(
refresh_token: session.refresh_token,
expires_at: session.expires_at,
tfa_bypass_until: session.tfa_bypass_until,
user: {
id: user.id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
role: user.role,
},
user: updatedUser,
});
} catch (error) {
console.error("TFA verification error:", error);
@@ -1014,13 +1024,27 @@ router.put(
.withMessage("Username must be at least 3 characters"),
body("email").optional().isEmail().withMessage("Valid email is required"),
body("first_name")
.optional()
.isLength({ min: 1 })
.withMessage("First name must be at least 1 character"),
.optional({ nullable: true, checkFalsy: true })
.custom((value) => {
// 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")
.optional()
.isLength({ min: 1 })
.withMessage("Last name must be at least 1 character"),
.optional({ nullable: true, checkFalsy: true })
.custom((value) => {
// 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) => {
try {
@@ -1034,16 +1058,22 @@ router.put(
updated_at: new Date(),
};
if (username) updateData.username = username;
if (email) updateData.email = email;
// Handle first_name and last_name - allow empty strings to clear the field
// Handle all fields consistently - trim and update if provided
if (username) updateData.username = username.trim();
if (email) updateData.email = email.trim();
if (first_name !== undefined) {
// Allow null or empty string to clear the field, otherwise trim
updateData.first_name =
first_name === "" ? null : first_name.trim() || null;
first_name === "" || first_name === null
? null
: first_name.trim() || null;
}
if (last_name !== undefined) {
// Allow null or empty string to clear the field, otherwise trim
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)
@@ -1106,16 +1136,6 @@ router.put(
// Use fresh data if available, otherwise fallback to 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({
message: "Profile updated successfully",
user: responseUser,

View File

@@ -450,8 +450,8 @@ function AppRoutes() {
function App() {
return (
<ThemeProvider>
<AuthProvider>
<AuthProvider>
<ThemeProvider>
<SettingsProvider>
<ColorThemeProvider>
<UpdateNotificationProvider>
@@ -461,8 +461,8 @@ function App() {
</UpdateNotificationProvider>
</ColorThemeProvider>
</SettingsProvider>
</AuthProvider>
</ThemeProvider>
</ThemeProvider>
</AuthProvider>
);
}

View File

@@ -138,6 +138,9 @@ export const AuthProvider = ({ children }) => {
setPermissions(userPermissions);
}
// Note: User preferences will be automatically fetched by ColorThemeContext
// when the component mounts, so no need to invalidate here
return { success: true };
} else {
// Handle HTTP error responses (like 500 CORS errors)
@@ -224,8 +227,6 @@ export const AuthProvider = ({ children }) => {
const data = await response.json();
if (response.ok) {
console.log("Profile updated - received user data:", data.user);
// Validate that we received user data with expected fields
if (!data.user || !data.user.id) {
console.error("Invalid user data in response:", data);
@@ -239,15 +240,6 @@ export const AuthProvider = ({ children }) => {
setUser(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 };
} else {
// Handle HTTP error responses (like 500 CORS errors)

View File

@@ -1,6 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { createContext, useContext, useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { userPreferencesAPI } from "../utils/api";
import { useAuth } from "./AuthContext";
const ColorThemeContext = createContext();
@@ -123,48 +132,108 @@ export const THEME_PRESETS = {
};
export const ColorThemeProvider = ({ children }) => {
const [colorTheme, setColorTheme] = useState(() => {
// Initialize from localStorage for immediate render
return localStorage.getItem("colorTheme") || "cyber_blue";
});
const [isLoading, setIsLoading] = useState(true);
const queryClient = useQueryClient();
const lastThemeRef = useRef(null);
// Fetch user preferences from backend
const { data: userPreferences } = useQuery({
// Use reactive authentication state from AuthContext
// 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"],
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
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(() => {
if (userPreferences?.color_theme) {
setColorTheme(userPreferences.color_theme);
localStorage.setItem("colorTheme", userPreferences.color_theme);
}
setIsLoading(false);
}, [userPreferences]);
}, [userPreferences?.color_theme]);
const updateColorTheme = async (theme) => {
setColorTheme(theme);
localStorage.setItem("colorTheme", theme);
const updateColorTheme = useCallback(
async (theme) => {
// Store previous theme for potential revert
const previousTheme = colorTheme;
// Save to backend
try {
await userPreferencesAPI.update({ color_theme: theme });
} catch (error) {
console.error("Failed to save color theme preference:", error);
// Theme is already set locally, so user still sees the change
}
};
// Immediately update state for instant UI feedback
setColorTheme(theme);
lastThemeRef.current = theme;
const value = {
colorTheme,
setColorTheme: updateColorTheme,
themeConfig: THEME_PRESETS[colorTheme] || THEME_PRESETS.default,
isLoading,
};
// Also update localStorage cache
localStorage.setItem("colorTheme", theme);
// Save to backend (source of truth)
try {
await userPreferencesAPI.update({ color_theme: theme });
// Invalidate and refetch user preferences to ensure sync across tabs/browsers
await queryClient.invalidateQueries({ queryKey: ["userPreferences"] });
} catch (error) {
console.error("Failed to save color theme preference:", error);
// Revert to previous theme if save failed
setColorTheme(previousTheme);
lastThemeRef.current = previousTheme;
localStorage.setItem("colorTheme", previousTheme);
// 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,
setColorTheme: updateColorTheme,
themeConfig,
isLoading,
}),
[colorTheme, themeConfig, isLoading, updateColorTheme],
);
return (
<ColorThemeContext.Provider value={value}>

View File

@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { createContext, useContext, useEffect, useState } from "react";
import { userPreferencesAPI } from "../utils/api";
import { useAuth } from "./AuthContext";
const ThemeContext = createContext();
@@ -26,21 +27,29 @@ export const ThemeProvider = ({ children }) => {
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({
queryKey: ["userPreferences"],
queryFn: () => userPreferencesAPI.get().then((res) => res.data),
enabled: isAuthenticated, // Only run query if user is authenticated
retry: 1,
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Sync with user preferences from backend
// Sync with user preferences from backend or user object from login
useEffect(() => {
if (userPreferences?.theme_preference) {
setTheme(userPreferences.theme_preference);
localStorage.setItem("theme", userPreferences.theme_preference);
const preferredTheme =
userPreferences?.theme_preference || user?.theme_preference;
if (preferredTheme) {
setTheme(preferredTheme);
localStorage.setItem("theme", preferredTheme);
}
}, [userPreferences]);
}, [userPreferences, user?.theme_preference]);
useEffect(() => {
// Apply theme to document

View File

@@ -248,7 +248,8 @@ const Login = () => {
} catch (error) {
console.error("Failed to fetch GitHub data:", error);
// Set fallback data if nothing cached
if (!latestRelease) {
const cachedRelease = localStorage.getItem("githubLatestRelease");
if (!cachedRelease) {
setLatestRelease({
version: "v1.3.0",
name: "Latest Release",
@@ -260,7 +261,7 @@ const Login = () => {
};
fetchGitHubData();
}, [latestRelease]);
}, []); // Run once on mount
const handleSubmit = async (e) => {
e.preventDefault();