feat: improve authentication
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
82
core/src/services/authManager.ts
Normal file
82
core/src/services/authManager.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Authentication Manager
|
||||
*/
|
||||
|
||||
const REFRESH_URL = '/auth/refresh';
|
||||
const PING_URL = '/auth/ping';
|
||||
const TOKEN_EXPIRY_BUFFER_MS = 60 * 1000;
|
||||
const PING_INTERVAL_MS = 60 * 1000;
|
||||
|
||||
let tokenExpiresAt: number | null = null;
|
||||
let refreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
export function recordExpiry(expiresIn: number | null | undefined): void {
|
||||
tokenExpiresAt = expiresIn != null ? Date.now() + expiresIn * 1000 : null;
|
||||
}
|
||||
|
||||
function isTokenExpiredOrExpiring(): boolean {
|
||||
if (tokenExpiresAt === null) return false;
|
||||
return Date.now() >= tokenExpiresAt - TOKEN_EXPIRY_BUFFER_MS;
|
||||
}
|
||||
|
||||
async function refreshToken(): Promise<boolean> {
|
||||
if (refreshPromise) return refreshPromise;
|
||||
|
||||
refreshPromise = fetch(REFRESH_URL, { method: 'POST', credentials: 'include' })
|
||||
.then(async r => {
|
||||
if (!r.ok) return false;
|
||||
try {
|
||||
const body = await r.clone().json();
|
||||
recordExpiry(body?.expires_in);
|
||||
} catch { /* ignore parse errors */ }
|
||||
return true;
|
||||
})
|
||||
.catch(() => false)
|
||||
.finally(() => { refreshPromise = null; });
|
||||
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the token is fresh before a request fires.
|
||||
*/
|
||||
export async function ensureFreshToken(): Promise<void> {
|
||||
if (isTokenExpiredOrExpiring()) {
|
||||
await refreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
export interface SessionMonitorOptions {
|
||||
onLogout: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function createSessionMonitor(options: SessionMonitorOptions) {
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function ping(): Promise<void> {
|
||||
// GET ping — no body, no CSRF needed, just confirm the session cookie is still valid
|
||||
const response = await fetch(PING_URL, { credentials: 'include' }).catch(() => null);
|
||||
|
||||
if (!response?.ok) {
|
||||
// Session may be expired — attempt a token refresh before giving up
|
||||
const refreshed = await refreshToken();
|
||||
if (!refreshed) {
|
||||
stop();
|
||||
await options.onLogout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function start(): void {
|
||||
if (intervalId !== null) return;
|
||||
intervalId = setInterval(ping, PING_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
if (intervalId === null) return;
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
|
||||
return { start, stop };
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export const authenticationService = {
|
||||
* Initialize authentication - get session and available methods
|
||||
*/
|
||||
async start(): Promise<StartResponse> {
|
||||
return fetchWrapper.get('/auth/start', undefined, { skipLogoutOnError: true });
|
||||
return fetchWrapper.get('/auth/start');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -20,7 +20,7 @@ export const authenticationService = {
|
||||
return fetchWrapper.post('/auth/identify', {
|
||||
session,
|
||||
identity,
|
||||
}, { skipLogoutOnError: true });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ export const authenticationService = {
|
||||
method,
|
||||
response,
|
||||
...(identity && { identity }),
|
||||
}, { autoRetry: false, skipLogoutOnError: true });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -57,7 +57,7 @@ export const authenticationService = {
|
||||
session,
|
||||
method,
|
||||
return_url: returnUrl,
|
||||
}, { skipLogoutOnError: true });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -67,14 +67,14 @@ export const authenticationService = {
|
||||
return fetchWrapper.post('/auth/challenge', {
|
||||
session,
|
||||
method,
|
||||
}, { skipLogoutOnError: true });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current session status
|
||||
*/
|
||||
async getStatus(session: string): Promise<SessionStatus> {
|
||||
return fetchWrapper.get(`/auth/status?session=${encodeURIComponent(session)}`, undefined, { skipLogoutOnError: true });
|
||||
return fetchWrapper.get(`/auth/status?session=${encodeURIComponent(session)}`);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user