mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
feat: update session cookie options and enhance authentication flow
- Changed SameSite attribute for session cookies from 'strict' to 'lax' to allow cross-origin fetches, improving compatibility with various client requests. - Updated cookie clearing logic in the authentication route to use `res.cookie()` for better reliability in cross-origin environments. - Refactored the login view to implement a state machine for managing authentication phases, enhancing clarity and maintainability. - Introduced a new logged-out view to inform users of session expiration and provide options to log in or retry. - Added account and security sections to the settings view, allowing users to manage their account and security preferences more effectively.
This commit is contained in:
@@ -45,6 +45,36 @@ const logger = createLogger('HttpClient');
|
||||
// Cached server URL (set during initialization in Electron mode)
|
||||
let cachedServerUrl: string | null = null;
|
||||
|
||||
/**
|
||||
* Notify the UI that the current session is no longer valid.
|
||||
* Used to redirect the user to a logged-out route on 401/403 responses.
|
||||
*/
|
||||
const notifyLoggedOut = (): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('automaker:logged-out'));
|
||||
} catch {
|
||||
// Ignore - navigation will still be handled by failed requests in most cases
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle an unauthorized response in cookie/session auth flows.
|
||||
* Clears in-memory token and attempts to clear the cookie (best-effort),
|
||||
* then notifies the UI to redirect.
|
||||
*/
|
||||
const handleUnauthorized = (): void => {
|
||||
clearSessionToken();
|
||||
// Best-effort cookie clear (avoid throwing)
|
||||
fetch(`${getServerUrl()}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: '{}',
|
||||
}).catch(() => {});
|
||||
notifyLoggedOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize server URL from Electron IPC.
|
||||
* Must be called early in Electron mode before making API requests.
|
||||
@@ -88,6 +118,7 @@ let apiKeyInitialized = false;
|
||||
let apiKeyInitPromise: Promise<void> | null = null;
|
||||
|
||||
// Cached session token for authentication (Web mode - explicit header auth)
|
||||
// Only used in-memory after fresh login; on refresh we rely on HTTP-only cookies
|
||||
let cachedSessionToken: string | null = null;
|
||||
|
||||
// Get API key for Electron mode (returns cached value after initialization)
|
||||
@@ -105,10 +136,10 @@ export const waitForApiKeyInit = (): Promise<void> => {
|
||||
return initApiKey();
|
||||
};
|
||||
|
||||
// Get session token for Web mode (returns cached value after login or token fetch)
|
||||
// Get session token for Web mode (returns cached value after login)
|
||||
export const getSessionToken = (): string | null => cachedSessionToken;
|
||||
|
||||
// Set session token (called after login or token fetch)
|
||||
// Set session token (called after login)
|
||||
export const setSessionToken = (token: string | null): void => {
|
||||
cachedSessionToken = token;
|
||||
};
|
||||
@@ -311,6 +342,7 @@ export const logout = async (): Promise<{ success: boolean }> => {
|
||||
try {
|
||||
const response = await fetch(`${getServerUrl()}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
@@ -331,52 +363,52 @@ export const logout = async (): Promise<{ success: boolean }> => {
|
||||
* This should be called:
|
||||
* 1. After login to verify the cookie was set correctly
|
||||
* 2. On app load to verify the session hasn't expired
|
||||
*
|
||||
* Returns:
|
||||
* - true: Session is valid
|
||||
* - false: Session is definitively invalid (401/403 auth failure)
|
||||
* - throws: Network error or server not ready (caller should retry)
|
||||
*/
|
||||
export const verifySession = async (): Promise<boolean> => {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Add session token header if available
|
||||
const sessionToken = getSessionToken();
|
||||
if (sessionToken) {
|
||||
headers['X-Session-Token'] = sessionToken;
|
||||
}
|
||||
// Add session token header if available
|
||||
const sessionToken = getSessionToken();
|
||||
if (sessionToken) {
|
||||
headers['X-Session-Token'] = sessionToken;
|
||||
}
|
||||
|
||||
// Make a request to an authenticated endpoint to verify the session
|
||||
// We use /api/settings/status as it requires authentication and is lightweight
|
||||
const response = await fetch(`${getServerUrl()}/api/settings/status`, {
|
||||
headers,
|
||||
credentials: 'include',
|
||||
});
|
||||
// Make a request to an authenticated endpoint to verify the session
|
||||
// We use /api/settings/status as it requires authentication and is lightweight
|
||||
// Note: fetch throws on network errors, which we intentionally let propagate
|
||||
const response = await fetch(`${getServerUrl()}/api/settings/status`, {
|
||||
headers,
|
||||
credentials: 'include',
|
||||
// Avoid hanging indefinitely during backend reloads or network issues
|
||||
signal: AbortSignal.timeout(2500),
|
||||
});
|
||||
|
||||
// Check for authentication errors
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
logger.warn('Session verification failed - session expired or invalid');
|
||||
// Clear the session since it's no longer valid
|
||||
clearSessionToken();
|
||||
// Try to clear the cookie via logout (fire and forget)
|
||||
fetch(`${getServerUrl()}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: '{}',
|
||||
}).catch(() => {});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn('Session verification failed with status:', response.status);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info('Session verified successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Session verification error:', error);
|
||||
// Check for authentication errors - these are definitive "invalid session" responses
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
logger.warn('Session verification failed - session expired or invalid');
|
||||
// Clear the in-memory/localStorage session token since it's no longer valid
|
||||
// Note: We do NOT call logout here - that would destroy a potentially valid
|
||||
// cookie if the issue was transient (e.g., token not sent due to timing)
|
||||
clearSessionToken();
|
||||
return false;
|
||||
}
|
||||
|
||||
// For other non-ok responses (5xx, etc.), throw to trigger retry
|
||||
if (!response.ok) {
|
||||
const error = new Error(`Session verification failed with status: ${response.status}`);
|
||||
logger.warn('Session verification failed with status:', response.status);
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info('Session verified successfully');
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -472,6 +504,11 @@ export class HttpApiClient implements ElectronAPI {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
handleUnauthorized();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to fetch wsToken:', response.status);
|
||||
return null;
|
||||
@@ -653,6 +690,11 @@ export class HttpApiClient implements ElectronAPI {
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
handleUnauthorized();
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
@@ -677,6 +719,11 @@ export class HttpApiClient implements ElectronAPI {
|
||||
credentials: 'include', // Include cookies for session auth
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
handleUnauthorized();
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
@@ -703,6 +750,11 @@ export class HttpApiClient implements ElectronAPI {
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
handleUnauthorized();
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
@@ -728,6 +780,11 @@ export class HttpApiClient implements ElectronAPI {
|
||||
credentials: 'include', // Include cookies for session auth
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
handleUnauthorized();
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user