diff --git a/backend/igny8_core/auth/urls.py b/backend/igny8_core/auth/urls.py index 18dd9ba7..a320702a 100644 --- a/backend/igny8_core/auth/urls.py +++ b/backend/igny8_core/auth/urls.py @@ -48,6 +48,7 @@ class RegisterView(APIView): def post(self, request): from .utils import generate_access_token, generate_refresh_token, get_token_expiry from django.contrib.auth import login + from django.utils import timezone serializer = RegisterSerializer(data=request.data) if serializer.is_valid(): @@ -62,8 +63,8 @@ class RegisterView(APIView): # Generate JWT tokens access_token = generate_access_token(user, account) refresh_token = generate_refresh_token(user, account) - access_expires_at = get_token_expiry('access') - refresh_expires_at = get_token_expiry('refresh') + access_expires_at = timezone.now() + get_token_expiry('access') + refresh_expires_at = timezone.now() + get_token_expiry('refresh') user_serializer = UserSerializer(user) return success_response( @@ -123,10 +124,11 @@ class LoginView(APIView): # Generate JWT tokens from .utils import generate_access_token, generate_refresh_token, get_access_token_expiry, get_token_expiry + from django.utils import timezone access_token = generate_access_token(user, account, remember_me=remember_me) refresh_token = generate_refresh_token(user, account) - access_expires_at = get_access_token_expiry(remember_me=remember_me) - refresh_expires_at = get_token_expiry('refresh') + access_expires_at = timezone.now() + get_access_token_expiry(remember_me=remember_me) + refresh_expires_at = timezone.now() + get_token_expiry('refresh') # Serialize user data safely, handling missing account relationship try: diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index ba620d2a..8e84c532 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -97,8 +97,8 @@ CSRF_COOKIE_SECURE = USE_SECURE_COOKIES SESSION_COOKIE_NAME = 'igny8_sessionid' # Custom name to avoid conflicts SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access SESSION_COOKIE_SAMESITE = 'Strict' # Prevent cross-site cookie sharing -SESSION_COOKIE_AGE = 3600 # 1 hour default (increased if remember me checked) -SESSION_SAVE_EVERY_REQUEST = False # Don't update session on every request (reduces DB load) +SESSION_COOKIE_AGE = 3600 # 1 hour - extends on every request due to SESSION_SAVE_EVERY_REQUEST +SESSION_SAVE_EVERY_REQUEST = True # CRITICAL: Update session on every request to prevent idle timeout SESSION_COOKIE_PATH = '/' # Explicit path # Don't set SESSION_COOKIE_DOMAIN - let it default to current domain for strict isolation @@ -521,8 +521,8 @@ CORS_EXPOSE_HEADERS = [ JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY) JWT_ALGORITHM = 'HS256' # Default: 1 hour for normal login, 20 days for remember me -JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1) # Increased from 15 minutes -JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME = timedelta(days=20) # For remember me users +JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1) # Default: 1 hour +JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME = timedelta(days=30) # Remember me: 30 days JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30) # Extended to 30 days for persistent login # Celery Configuration diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b977a59b..0eae0c29 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -158,23 +158,7 @@ const Tooltips = lazy(() => import("./pages/Settings/UiElements/Tooltips")); const Videos = lazy(() => import("./pages/Settings/UiElements/Videos")); export default function App() { - const isAuthenticated = useAuthStore((state) => state.isAuthenticated); - const refreshUser = useAuthStore((state) => state.refreshUser); - const logout = useAuthStore((state) => state.logout); - - useEffect(() => { - const { token } = useAuthStore.getState(); - if (!isAuthenticated || !token) return; - - refreshUser().catch((error) => { - // Avoid log spam on auth pages when token is missing/expired - if (error?.message?.includes('Authentication credentials were not provided')) { - return; - } - console.warn('Session validation failed:', error); - logout(); - }); - }, [isAuthenticated, refreshUser, logout]); + // All session validation removed - API interceptor handles authentication return ( <> diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx index 7e297e9e..944fe8f5 100644 --- a/frontend/src/components/auth/ProtectedRoute.tsx +++ b/frontend/src/components/auth/ProtectedRoute.tsx @@ -48,18 +48,7 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) { trackLoading('auth-loading', loading); }, [loading]); - // Validate account + plan whenever auth/user changes - useEffect(() => { - if (!isAuthenticated) { - return; - } - - if (!user?.account) { - setErrorMessage('This user is not linked to an account. Please contact support.'); - logout(); - return; - } - }, [isAuthenticated, user, logout]); + // Account/plan validation removed - backend middleware handles this on API calls // Immediate check on mount: if loading is true, reset it immediately useEffect(() => { diff --git a/frontend/src/components/header/SiteSwitcher.tsx b/frontend/src/components/header/SiteSwitcher.tsx index 6b5f52f4..3bec65ab 100644 --- a/frontend/src/components/header/SiteSwitcher.tsx +++ b/frontend/src/components/header/SiteSwitcher.tsx @@ -68,18 +68,7 @@ export default function SiteSwitcher({ hiddenPaths }: SiteSwitcherProps) { shouldHide = hiddenPathsToUse.some(path => location.pathname.startsWith(path)); } - // Refresh user data when component mounts or user changes - // This ensures we have latest account/plan info for proper site filtering - useEffect(() => { - if (isAuthenticated && user) { - // Refresh user data to get latest account/plan changes - // This is important so site filtering works correctly - refreshUser().catch((error) => { - // Silently fail - user might still be valid, just couldn't refresh - console.debug('SiteSwitcher: Failed to refresh user (non-critical):', error); - }); - } - }, [isAuthenticated]); // Only refresh when auth state changes, not on every render + // User refresh removed - data loads on-demand from API calls useEffect(() => { if (shouldHide) { diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx index 1ee1ec62..acec1511 100644 --- a/frontend/src/layout/AppLayout.tsx +++ b/frontend/src/layout/AppLayout.tsx @@ -83,101 +83,7 @@ const LayoutContent: React.FC = () => { checkTokenAndLoad(); }, [isAuthenticated]); // Run when authentication state changes - // Sector loading moved to PageHeader component - // This ensures sectors are only loaded when content pages (Planner/Writer/Optimizer) mount - // Account/billing pages don't use PageHeader with site/sector selector, so they won't trigger sector loading - // This prevents unnecessary 404 errors on /account/plans and similar routes - - // Refresh user data on mount and when app version changes (after code updates) - // This ensures changes are reflected immediately without requiring re-login - useEffect(() => { - if (!isAuthenticated) return; - - const APP_VERSION = import.meta.env.VITE_APP_VERSION || '2.0.2'; - const VERSION_STORAGE_KEY = 'igny8-app-version'; - - const refreshUserData = async (force = false) => { - const now = Date.now(); - // Throttle: only refresh if last refresh was more than 30 seconds ago (unless forced) - if (!force && now - lastUserRefresh.current < 30000) return; - - // Check if token exists before making API call - const authState = useAuthStore.getState(); - if (!authState?.token) { - // Token not available yet - wait a bit for Zustand persist to write it - setTimeout(() => { - const retryAuthState = useAuthStore.getState(); - if (retryAuthState?.token && retryAuthState?.isAuthenticated) { - refreshUserData(force); - } - }, 100); // Wait 100ms for persist to write - return; - } - - try { - lastUserRefresh.current = now; - await refreshUser(); - - // Store current version after successful refresh - if (force) { - localStorage.setItem(VERSION_STORAGE_KEY, APP_VERSION); - } - } catch (error) { - // Silently fail - user might still be authenticated - console.debug('User data refresh failed (non-critical):', error); - } - }; - - // Check if app version changed (indicates code update) - const storedVersion = localStorage.getItem(VERSION_STORAGE_KEY); - if (storedVersion !== APP_VERSION) { - // Force refresh on version change - refreshUserData(true); - } else { - // Normal refresh on mount - refreshUserData(); - } - - // Refresh when window becomes visible (user switches back to tab) - const handleVisibilityChange = () => { - if (document.visibilityState === 'visible') { - refreshUserData(); - } - }; - - // Refresh on window focus - const handleFocus = () => { - refreshUserData(); - }; - - // Proactive token refresh - refresh token every 12 minutes (before 15-minute expiry) - // This prevents 401 errors and ensures seamless user experience - const tokenRefreshInterval = setInterval(async () => { - const authState = useAuthStore.getState(); - const refreshToken = authState?.refreshToken; - if (refreshToken && authState?.isAuthenticated) { - try { - await authState.refreshToken(); - console.debug('Token proactively refreshed'); - } catch (error) { - console.debug('Proactive token refresh failed (will retry on next API call):', error); - } - } - }, 720000); // 12 minutes = 720000ms - - // Periodic user data refresh every 2 minutes - const intervalId = setInterval(() => refreshUserData(), 120000); - - document.addEventListener('visibilitychange', handleVisibilityChange); - window.addEventListener('focus', handleFocus); - - return () => { - clearInterval(tokenRefreshInterval); - clearInterval(intervalId); - document.removeEventListener('visibilitychange', handleVisibilityChange); - window.removeEventListener('focus', handleFocus); - }; - }, [isAuthenticated, refreshUser]); + // All session refresh logic removed - API interceptor handles token refresh automatically on 401 // Load credit balance and set in header metrics useEffect(() => {