Compare commits

...

4 Commits

Author SHA1 Message Date
Muhammad Ibrahim
e57ff7612e removed emoji 2025-11-01 03:25:31 +00:00
Muhammad Ibrahim
7a3d98862f Fix emoji parsing error in print functions
- Changed from echo -e to printf for safer special character handling
- Store emoji characters in variables using bash octal escape sequences
- Prevents 'command not found' error when bash interprets emoji as commands
- Fixes issue where line 41 error occurred during setup.sh --update
2025-11-01 02:58:34 +00:00
9 Technology Group LTD
913976b7f6 1.3.2 final
fixed theme and user profile settings
2025-10-31 22:25:22 +00:00
Muhammad Ibrahim
53ff3bb1e2 fixed theme and user profile settings 2025-10-31 22:17:24 +00:00
7 changed files with 198 additions and 107 deletions

View File

@@ -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,

View File

@@ -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>
); );
} }

View File

@@ -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)

View File

@@ -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}>

View File

@@ -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

View File

@@ -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();

View File

@@ -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