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 ( + { + if (!open) { + // Prevent closing dialog by clicking outside + // User must click a button + return; + } + }}> + e.preventDefault()} + onEscapeKeyDown={(e) => e.preventDefault()} + > + +
+
+ +
+ + Session About to Expire + +
+ + Your session will expire in {timeRemaining} seconds due to inactivity. + +
+

+ Click "Stay Logged In" to continue your session, or you will be automatically logged out. +

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

*/} )}