logout issues # 2
This commit is contained in:
@@ -48,6 +48,7 @@ class RegisterView(APIView):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
from .utils import generate_access_token, generate_refresh_token, get_token_expiry
|
from .utils import generate_access_token, generate_refresh_token, get_token_expiry
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
serializer = RegisterSerializer(data=request.data)
|
serializer = RegisterSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@@ -62,8 +63,8 @@ class RegisterView(APIView):
|
|||||||
# Generate JWT tokens
|
# Generate JWT tokens
|
||||||
access_token = generate_access_token(user, account)
|
access_token = generate_access_token(user, account)
|
||||||
refresh_token = generate_refresh_token(user, account)
|
refresh_token = generate_refresh_token(user, account)
|
||||||
access_expires_at = get_token_expiry('access')
|
access_expires_at = timezone.now() + get_token_expiry('access')
|
||||||
refresh_expires_at = get_token_expiry('refresh')
|
refresh_expires_at = timezone.now() + get_token_expiry('refresh')
|
||||||
|
|
||||||
user_serializer = UserSerializer(user)
|
user_serializer = UserSerializer(user)
|
||||||
return success_response(
|
return success_response(
|
||||||
@@ -123,10 +124,11 @@ class LoginView(APIView):
|
|||||||
|
|
||||||
# Generate JWT tokens
|
# Generate JWT tokens
|
||||||
from .utils import generate_access_token, generate_refresh_token, get_access_token_expiry, get_token_expiry
|
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)
|
access_token = generate_access_token(user, account, remember_me=remember_me)
|
||||||
refresh_token = generate_refresh_token(user, account)
|
refresh_token = generate_refresh_token(user, account)
|
||||||
access_expires_at = get_access_token_expiry(remember_me=remember_me)
|
access_expires_at = timezone.now() + get_access_token_expiry(remember_me=remember_me)
|
||||||
refresh_expires_at = get_token_expiry('refresh')
|
refresh_expires_at = timezone.now() + get_token_expiry('refresh')
|
||||||
|
|
||||||
# Serialize user data safely, handling missing account relationship
|
# Serialize user data safely, handling missing account relationship
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ CSRF_COOKIE_SECURE = USE_SECURE_COOKIES
|
|||||||
SESSION_COOKIE_NAME = 'igny8_sessionid' # Custom name to avoid conflicts
|
SESSION_COOKIE_NAME = 'igny8_sessionid' # Custom name to avoid conflicts
|
||||||
SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access
|
SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access
|
||||||
SESSION_COOKIE_SAMESITE = 'Strict' # Prevent cross-site cookie sharing
|
SESSION_COOKIE_SAMESITE = 'Strict' # Prevent cross-site cookie sharing
|
||||||
SESSION_COOKIE_AGE = 3600 # 1 hour default (increased if remember me checked)
|
SESSION_COOKIE_AGE = 3600 # 1 hour - extends on every request due to SESSION_SAVE_EVERY_REQUEST
|
||||||
SESSION_SAVE_EVERY_REQUEST = False # Don't update session on every request (reduces DB load)
|
SESSION_SAVE_EVERY_REQUEST = True # CRITICAL: Update session on every request to prevent idle timeout
|
||||||
SESSION_COOKIE_PATH = '/' # Explicit path
|
SESSION_COOKIE_PATH = '/' # Explicit path
|
||||||
# Don't set SESSION_COOKIE_DOMAIN - let it default to current domain for strict isolation
|
# 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_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY)
|
||||||
JWT_ALGORITHM = 'HS256'
|
JWT_ALGORITHM = 'HS256'
|
||||||
# Default: 1 hour for normal login, 20 days for remember me
|
# 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 = timedelta(hours=1) # Default: 1 hour
|
||||||
JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME = timedelta(days=20) # For remember me users
|
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
|
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30) # Extended to 30 days for persistent login
|
||||||
|
|
||||||
# Celery Configuration
|
# Celery Configuration
|
||||||
|
|||||||
@@ -158,23 +158,7 @@ const Tooltips = lazy(() => import("./pages/Settings/UiElements/Tooltips"));
|
|||||||
const Videos = lazy(() => import("./pages/Settings/UiElements/Videos"));
|
const Videos = lazy(() => import("./pages/Settings/UiElements/Videos"));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
// All session validation removed - API interceptor handles authentication
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -48,18 +48,7 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
|||||||
trackLoading('auth-loading', loading);
|
trackLoading('auth-loading', loading);
|
||||||
}, [loading]);
|
}, [loading]);
|
||||||
|
|
||||||
// Validate account + plan whenever auth/user changes
|
// Account/plan validation removed - backend middleware handles this on API calls
|
||||||
useEffect(() => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user?.account) {
|
|
||||||
setErrorMessage('This user is not linked to an account. Please contact support.');
|
|
||||||
logout();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, user, logout]);
|
|
||||||
|
|
||||||
// Immediate check on mount: if loading is true, reset it immediately
|
// Immediate check on mount: if loading is true, reset it immediately
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -68,18 +68,7 @@ export default function SiteSwitcher({ hiddenPaths }: SiteSwitcherProps) {
|
|||||||
shouldHide = hiddenPathsToUse.some(path => location.pathname.startsWith(path));
|
shouldHide = hiddenPathsToUse.some(path => location.pathname.startsWith(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh user data when component mounts or user changes
|
// User refresh removed - data loads on-demand from API calls
|
||||||
// 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
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldHide) {
|
if (shouldHide) {
|
||||||
|
|||||||
@@ -83,101 +83,7 @@ const LayoutContent: React.FC = () => {
|
|||||||
checkTokenAndLoad();
|
checkTokenAndLoad();
|
||||||
}, [isAuthenticated]); // Run when authentication state changes
|
}, [isAuthenticated]); // Run when authentication state changes
|
||||||
|
|
||||||
// Sector loading moved to PageHeader component
|
// All session refresh logic removed - API interceptor handles token refresh automatically on 401
|
||||||
// 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]);
|
|
||||||
|
|
||||||
// Load credit balance and set in header metrics
|
// Load credit balance and set in header metrics
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user