From c54eb23689f11dbe3492cb8f3d80f3e379c05b39 Mon Sep 17 00:00:00 2001
From: Koncept Kit <63216427+konceptkit@users.noreply.github.com>
Date: Wed, 7 Jan 2026 13:37:20 +0700
Subject: [PATCH 1/4] Login and Session Fixes
---
public/health.json | 5 +
src/App.js | 2 +
src/components/IdleSessionWarning.js | 231 +++++++++++++++++++++++++++
src/context/AuthContext.js | 60 ++++---
4 files changed, 275 insertions(+), 23 deletions(-)
create mode 100644 public/health.json
create mode 100644 src/components/IdleSessionWarning.js
diff --git a/public/health.json b/public/health.json
new file mode 100644
index 0000000..213db9a
--- /dev/null
+++ b/public/health.json
@@ -0,0 +1,5 @@
+{
+ "status": "healthy",
+ "mode": "production",
+ "build": "optimized"
+}
diff --git a/src/App.js b/src/App.js
index d706f13..ed23527 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,7 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Toaster } from './components/ui/sonner';
+import IdleSessionWarning from './components/IdleSessionWarning';
import Landing from './pages/Landing';
import Register from './pages/Register';
import Login from './pages/Login';
@@ -294,6 +295,7 @@ function App() {
} />
+
);
diff --git a/src/components/IdleSessionWarning.js b/src/components/IdleSessionWarning.js
new file mode 100644
index 0000000..1b6763f
--- /dev/null
+++ b/src/components/IdleSessionWarning.js
@@ -0,0 +1,231 @@
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from './ui/dialog';
+import { Button } from './ui/button';
+import { AlertTriangle, RefreshCw } from 'lucide-react';
+
+/**
+ * IdleSessionWarning Component
+ *
+ * Monitors user activity and warns before session expiration
+ * - Warns 1 minute before JWT expiry (at 29 minutes if JWT is 30 min)
+ * - Auto-logout on expiration
+ * - "Stay Logged In" extends session
+ */
+const IdleSessionWarning = () => {
+ const { user, logout, refreshUser } = useAuth();
+ const navigate = useNavigate();
+
+ // Configuration
+ const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds
+ const WARNING_BEFORE_EXPIRY = 1 * 60 * 1000; // Warn 1 minute before expiry
+ const WARNING_TIME = SESSION_DURATION - WARNING_BEFORE_EXPIRY; // 29 minutes
+
+ const [showWarning, setShowWarning] = useState(false);
+ const [timeRemaining, setTimeRemaining] = useState(60); // seconds
+ const [isExtending, setIsExtending] = useState(false);
+
+ const activityTimeoutRef = useRef(null);
+ const warningTimeoutRef = useRef(null);
+ const countdownIntervalRef = useRef(null);
+ const lastActivityRef = useRef(Date.now());
+
+ // Reset activity timer
+ const resetActivityTimer = useCallback(() => {
+ lastActivityRef.current = Date.now();
+
+ // Clear existing timers
+ if (activityTimeoutRef.current) {
+ clearTimeout(activityTimeoutRef.current);
+ }
+ if (warningTimeoutRef.current) {
+ clearTimeout(warningTimeoutRef.current);
+ }
+ if (countdownIntervalRef.current) {
+ clearInterval(countdownIntervalRef.current);
+ }
+
+ // Hide warning if showing
+ if (showWarning) {
+ setShowWarning(false);
+ }
+
+ // Set new warning timer
+ warningTimeoutRef.current = setTimeout(() => {
+ // Show warning
+ setShowWarning(true);
+ setTimeRemaining(60); // 60 seconds until logout
+
+ // Start countdown
+ countdownIntervalRef.current = setInterval(() => {
+ setTimeRemaining((prev) => {
+ if (prev <= 1) {
+ // Time's up - logout
+ handleSessionExpired();
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ // Set auto-logout timer
+ activityTimeoutRef.current = setTimeout(() => {
+ handleSessionExpired();
+ }, WARNING_BEFORE_EXPIRY);
+
+ }, WARNING_TIME);
+ }, [showWarning]);
+
+ // Handle session expiration
+ const handleSessionExpired = useCallback(() => {
+ // Clear all timers
+ if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current);
+ if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current);
+ if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
+
+ setShowWarning(false);
+ logout();
+ navigate('/login', {
+ state: { message: 'Your session has expired due to inactivity. Please log in again.' }
+ });
+ }, [logout, navigate]);
+
+ // Handle "Stay Logged In" button
+ const handleExtendSession = async () => {
+ setIsExtending(true);
+ try {
+ // Refresh user data to get new token
+ await refreshUser();
+
+ // Reset activity timer
+ resetActivityTimer();
+
+ console.log('[IdleSessionWarning] Session extended successfully');
+ } catch (error) {
+ console.error('[IdleSessionWarning] Failed to extend session:', error);
+
+ // If refresh fails, logout
+ handleSessionExpired();
+ } finally {
+ setIsExtending(false);
+ }
+ };
+
+ // Track user activity
+ useEffect(() => {
+ if (!user) return;
+
+ const activityEvents = [
+ 'mousedown',
+ 'mousemove',
+ 'keypress',
+ 'scroll',
+ 'touchstart',
+ 'click'
+ ];
+
+ // Throttle activity detection to avoid too many resets
+ let throttleTimeout = null;
+ const handleActivity = () => {
+ if (throttleTimeout) return;
+
+ throttleTimeout = setTimeout(() => {
+ resetActivityTimer();
+ throttleTimeout = null;
+ }, 1000); // Throttle to once per second
+ };
+
+ // Add event listeners
+ activityEvents.forEach(event => {
+ document.addEventListener(event, handleActivity, { passive: true });
+ });
+
+ // Initialize timer
+ resetActivityTimer();
+
+ // Cleanup
+ return () => {
+ activityEvents.forEach(event => {
+ document.removeEventListener(event, handleActivity);
+ });
+
+ if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current);
+ if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current);
+ if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
+ if (throttleTimeout) clearTimeout(throttleTimeout);
+ };
+ }, [user, resetActivityTimer]);
+
+ // Don't render if user is not logged in
+ if (!user) return null;
+
+ return (
+
+ );
+};
+
+export default IdleSessionWarning;
diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js
index 24df9bf..f05e6d8 100644
--- a/src/context/AuthContext.js
+++ b/src/context/AuthContext.js
@@ -1,5 +1,6 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';
+import api from '../utils/api';
const AuthContext = createContext();
@@ -68,11 +69,11 @@ export const AuthProvider = ({ children }) => {
fullUrl: `${API_URL}/api/auth/login`
});
- const response = await axios.post(
- `${API_URL}/api/auth/login`,
+ // Use api instance for retry logic
+ const response = await api.post(
+ '/auth/login',
{ email, password },
{
- timeout: 30000, // 30 second timeout
headers: {
'Content-Type': 'application/json'
}
@@ -87,11 +88,19 @@ export const AuthProvider = ({ children }) => {
const { access_token, user: userData } = response.data;
- // Store token first
- localStorage.setItem('token', access_token);
- console.log('[AuthContext] Token stored in localStorage');
+ if (!access_token || !userData) {
+ throw new Error('Invalid response from server - missing token or user data');
+ }
- // Update state
+ // Store token FIRST and verify it was stored
+ localStorage.setItem('token', access_token);
+ const storedToken = localStorage.getItem('token');
+ if (storedToken !== access_token) {
+ throw new Error('Failed to store token in localStorage');
+ }
+ console.log('[AuthContext] Token stored and verified in localStorage');
+
+ // Update state in correct order
setToken(access_token);
setUser(userData);
console.log('[AuthContext] User state updated:', {
@@ -99,22 +108,21 @@ export const AuthProvider = ({ children }) => {
role: userData.role
});
- // Fetch user permissions (don't let this fail the login)
- // Use setTimeout to defer permission fetching slightly
- setTimeout(async () => {
- try {
- console.log('[AuthContext] Fetching permissions...');
- await fetchPermissions(access_token);
- console.log('[AuthContext] Permissions fetched successfully');
- } catch (error) {
- console.error('[AuthContext] Failed to fetch permissions (non-critical):', {
- message: error.message,
- response: error.response?.data,
- status: error.response?.status
- });
- // Don't throw - permissions can be fetched later if needed
- }
- }, 100); // Small delay to ensure state is settled
+ // Fetch permissions immediately and WAIT for it (but don't fail login if it fails)
+ try {
+ console.log('[AuthContext] Fetching permissions...');
+ await fetchPermissions(access_token);
+ console.log('[AuthContext] Permissions fetched successfully');
+ } catch (permError) {
+ console.error('[AuthContext] Failed to fetch permissions (non-critical):', {
+ message: permError.message,
+ response: permError.response?.data,
+ status: permError.response?.status
+ });
+ // Set empty permissions array so hasPermission doesn't break
+ setPermissions([]);
+ // Don't throw - login succeeded even if permissions failed
+ }
return userData;
} catch (error) {
@@ -131,6 +139,12 @@ export const AuthProvider = ({ children }) => {
}
});
+ // Clear any partial state
+ localStorage.removeItem('token');
+ setToken(null);
+ setUser(null);
+ setPermissions([]);
+
// Re-throw to let Login component handle the error
throw error;
}
From 5377a0f465c833d8de513cfe0d2ed6c55d97149e Mon Sep 17 00:00:00 2001
From: Koncept Kit <63216427+konceptkit@users.noreply.github.com>
Date: Wed, 7 Jan 2026 14:03:32 +0700
Subject: [PATCH 2/4] Security Hardening
---
src/components/IdleSessionWarning.js | 5 ++-
src/context/AuthContext.js | 23 +++++-----
src/utils/logger.js | 66 ++++++++++++++++++++++++++++
3 files changed, 81 insertions(+), 13 deletions(-)
create mode 100644 src/utils/logger.js
diff --git a/src/components/IdleSessionWarning.js b/src/components/IdleSessionWarning.js
index 1b6763f..adf450c 100644
--- a/src/components/IdleSessionWarning.js
+++ b/src/components/IdleSessionWarning.js
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
+import logger from '../utils/logger';
import {
Dialog,
DialogContent,
@@ -108,9 +109,9 @@ const IdleSessionWarning = () => {
// Reset activity timer
resetActivityTimer();
- console.log('[IdleSessionWarning] Session extended successfully');
+ logger.log('[IdleSessionWarning] Session extended successfully');
} catch (error) {
- console.error('[IdleSessionWarning] Failed to extend session:', error);
+ logger.error('[IdleSessionWarning] Failed to extend session:', error);
// If refresh fails, logout
handleSessionExpired();
diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js
index f05e6d8..5dae249 100644
--- a/src/context/AuthContext.js
+++ b/src/context/AuthContext.js
@@ -1,13 +1,14 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';
import api from '../utils/api';
+import logger from '../utils/logger';
const AuthContext = createContext();
const API_URL = process.env.REACT_APP_BACKEND_URL || window.location.origin;
// Log environment on module load for debugging
-console.log('[AuthContext] Module initialized with:', {
+logger.log('[AuthContext] Module initialized with:', {
REACT_APP_BACKEND_URL: process.env.REACT_APP_BACKEND_URL,
REACT_APP_BASENAME: process.env.REACT_APP_BASENAME,
API_URL: API_URL
@@ -56,14 +57,14 @@ export const AuthProvider = ({ children }) => {
});
setPermissions(response.data.permissions || []);
} catch (error) {
- console.error('Failed to fetch permissions:', error);
+ logger.error('Failed to fetch permissions:', error);
setPermissions([]);
}
};
const login = async (email, password) => {
try {
- console.log('[AuthContext] Starting login request...', {
+ logger.log('[AuthContext] Starting login request...', {
API_URL: API_URL,
envBackendUrl: process.env.REACT_APP_BACKEND_URL,
fullUrl: `${API_URL}/api/auth/login`
@@ -80,7 +81,7 @@ export const AuthProvider = ({ children }) => {
}
);
- console.log('[AuthContext] Login response received:', {
+ logger.log('[AuthContext] Login response received:', {
status: response.status,
hasToken: !!response.data?.access_token,
hasUser: !!response.data?.user
@@ -98,23 +99,23 @@ export const AuthProvider = ({ children }) => {
if (storedToken !== access_token) {
throw new Error('Failed to store token in localStorage');
}
- console.log('[AuthContext] Token stored and verified in localStorage');
+ logger.log('[AuthContext] Token stored and verified in localStorage');
// Update state in correct order
setToken(access_token);
setUser(userData);
- console.log('[AuthContext] User state updated:', {
+ logger.log('[AuthContext] User state updated:', {
email: userData.email,
role: userData.role
});
// Fetch permissions immediately and WAIT for it (but don't fail login if it fails)
try {
- console.log('[AuthContext] Fetching permissions...');
+ logger.log('[AuthContext] Fetching permissions...');
await fetchPermissions(access_token);
- console.log('[AuthContext] Permissions fetched successfully');
+ logger.log('[AuthContext] Permissions fetched successfully');
} catch (permError) {
- console.error('[AuthContext] Failed to fetch permissions (non-critical):', {
+ logger.error('[AuthContext] Failed to fetch permissions (non-critical):', {
message: permError.message,
response: permError.response?.data,
status: permError.response?.status
@@ -127,7 +128,7 @@ export const AuthProvider = ({ children }) => {
return userData;
} catch (error) {
// Enhanced error logging
- console.error('[AuthContext] Login failed:', {
+ logger.error('[AuthContext] Login failed:', {
message: error.message,
response: error.response?.data,
status: error.response?.status,
@@ -174,7 +175,7 @@ export const AuthProvider = ({ children }) => {
setUser(response.data);
return response.data;
} catch (error) {
- console.error('Failed to refresh user:', error);
+ logger.error('Failed to refresh user:', error);
// If token expired, logout
if (error.response?.status === 401) {
logout();
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 0000000..608c442
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,66 @@
+/**
+ * Production-safe logging utility
+ *
+ * In production (NODE_ENV=production), logs are disabled by default
+ * to prevent exposing sensitive information in browser console.
+ *
+ * In development, all logs are shown for debugging.
+ *
+ * Usage:
+ * import logger from '../utils/logger';
+ * logger.log('[Component]', 'message', data);
+ * logger.error('[Component]', 'error message', error);
+ * logger.warn('[Component]', 'warning message');
+ */
+
+const isDevelopment = process.env.NODE_ENV === 'development';
+
+// Force enable logs with REACT_APP_DEBUG_LOGS=true in .env
+const debugEnabled = process.env.REACT_APP_DEBUG_LOGS === 'true';
+
+const shouldLog = isDevelopment || debugEnabled;
+
+const logger = {
+ log: (...args) => {
+ if (shouldLog) {
+ console.log(...args);
+ }
+ },
+
+ error: (...args) => {
+ // Always log errors, but sanitize in production
+ if (shouldLog) {
+ console.error(...args);
+ } else {
+ // In production, only log error type without details
+ console.error('An error occurred. Enable debug logs for details.');
+ }
+ },
+
+ warn: (...args) => {
+ if (shouldLog) {
+ console.warn(...args);
+ }
+ },
+
+ info: (...args) => {
+ if (shouldLog) {
+ console.info(...args);
+ }
+ },
+
+ debug: (...args) => {
+ if (shouldLog) {
+ console.debug(...args);
+ }
+ },
+
+ // Special method for sensitive data - NEVER logs in production
+ sensitive: (...args) => {
+ if (isDevelopment) {
+ console.log('[SENSITIVE]', ...args);
+ }
+ }
+};
+
+export default logger;
From 180eb1ce857ccc88ae4ce44fc3418fd6297d19af Mon Sep 17 00:00:00 2001
From: Koncept Kit <63216427+konceptkit@users.noreply.github.com>
Date: Wed, 7 Jan 2026 15:37:40 +0700
Subject: [PATCH 3/4] Comment out View Public Site link on the AdminSidebar.js
---
src/components/AdminSidebar.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js
index fc70405..51a6f26 100644
--- a/src/components/AdminSidebar.js
+++ b/src/components/AdminSidebar.js
@@ -277,9 +277,9 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
Admin
-
+ {/*
View Public Site
-
+ */}
)}
From ee0ad176b0bf6a6329ef7427dae1a762aff1dec5 Mon Sep 17 00:00:00 2001
From: Koncept Kit <63216427+konceptkit@users.noreply.github.com>
Date: Fri, 9 Jan 2026 00:23:52 +0700
Subject: [PATCH 4/4] Remove View Public Site on AdminSidebar
---
src/components/AdminSidebar.js | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js
index 51a6f26..1e92537 100644
--- a/src/components/AdminSidebar.js
+++ b/src/components/AdminSidebar.js
@@ -277,9 +277,6 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
Admin
- {/*
- View Public Site
-
*/}
)}