u
This commit is contained in:
@@ -33,6 +33,7 @@ import { auth, company, number as numberApi } from '../api';
|
||||
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
|
||||
* @property {boolean} hasAuth True when the session has a valid authenticated user.
|
||||
* @property {boolean} hasCompany True when `hasAuth` is true AND a company is selected.
|
||||
* @property {string|null} retryNotification Message indicating a pending retry (e.g. 502 Bad Gateway).
|
||||
*/
|
||||
|
||||
/** Emitted after an authenticated session has been established. */
|
||||
@@ -49,11 +50,24 @@ const initialState = {
|
||||
status: 'loading',
|
||||
hasAuth: false,
|
||||
hasCompany: false,
|
||||
retryNotification: null,
|
||||
};
|
||||
|
||||
let emit = () => { };
|
||||
|
||||
// #region helpers
|
||||
/**
|
||||
* Detects whether an error represents an HTTP 502 Bad Gateway response.
|
||||
*
|
||||
* @param {any} err
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function is502BadGateway(err) {
|
||||
if (!err) return false;
|
||||
if (Number(err.status) === 502) return true;
|
||||
const msg = String(err.message || '');
|
||||
return /502|bad\s*gateway/i.test(msg);
|
||||
}
|
||||
/**
|
||||
* Registers the application-level dispatcher used for authentication events.
|
||||
*
|
||||
@@ -108,6 +122,7 @@ function buildAuthState(user, status, extra = {}) {
|
||||
status,
|
||||
hasAuth,
|
||||
hasCompany,
|
||||
retryNotification: null,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
@@ -126,52 +141,86 @@ function syncSse(state) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applySession(nextSession) {
|
||||
const googleClientId =
|
||||
nextSession.googleClientId ?? authAutomat.state.googleClientId;
|
||||
|
||||
if (!nextSession.user) {
|
||||
if (authAutomat.state.status !== 'authed') {
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(null, 'anon', { googleClientId, retryNotification: null })
|
||||
);
|
||||
syncSse(nextState);
|
||||
} else {
|
||||
authAutomat.setState({ googleClientId, retryNotification: null });
|
||||
}
|
||||
return nextSession;
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, nextSession.user);
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(user, 'authed', { googleClientId, retryNotification: null })
|
||||
);
|
||||
syncSse(nextState);
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return nextSession;
|
||||
}
|
||||
// #endregion
|
||||
|
||||
let authAutomat;
|
||||
let inFlightBootstrap = null;
|
||||
|
||||
const actions = {
|
||||
// #region bootstrapSession
|
||||
// MARK: calls auth.me from ../api
|
||||
/**
|
||||
* Restores an existing session or requests it from the authentication API.
|
||||
* Retries automatically every 5 seconds if a 502 Bad Gateway is received.
|
||||
*
|
||||
* @param {{user?: AuthUser|null, googleClientId?: string|null}|null} [session]
|
||||
* Optional preloaded session.
|
||||
* @returns {Promise<Object>} Resolved session response.
|
||||
*/
|
||||
async bootstrapSession(session) {
|
||||
try {
|
||||
const nextSession = session ?? await auth.me();
|
||||
const googleClientId =
|
||||
nextSession.googleClientId ?? authAutomat.state.googleClientId;
|
||||
|
||||
if (!nextSession.user) {
|
||||
if (authAutomat.state.status !== 'authed') {
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(null, 'anon', { googleClientId })
|
||||
);
|
||||
syncSse(nextState);
|
||||
} else {
|
||||
authAutomat.setState({ googleClientId });
|
||||
}
|
||||
return nextSession;
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, nextSession.user);
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(user, 'authed', { googleClientId })
|
||||
);
|
||||
syncSse(nextState);
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return nextSession;
|
||||
} catch (error) {
|
||||
if (authAutomat.state.status === 'loading') {
|
||||
const nextState = authAutomat.setState(buildAuthState(null, 'anon'));
|
||||
syncSse(nextState);
|
||||
}
|
||||
throw error;
|
||||
if (session) {
|
||||
return applySession(session);
|
||||
}
|
||||
|
||||
if (inFlightBootstrap) {
|
||||
return inFlightBootstrap;
|
||||
}
|
||||
|
||||
inFlightBootstrap = (async () => {
|
||||
let attempt = 0;
|
||||
while (true) {
|
||||
try {
|
||||
const nextSession = await auth.me();
|
||||
return applySession(nextSession);
|
||||
} catch (error) {
|
||||
if (is502BadGateway(error)) {
|
||||
attempt++;
|
||||
authAutomat.setState({
|
||||
retryNotification: `Server nicht erreichbar (502 Bad Gateway) – erneuter Versuch in 5s…${attempt > 1 ? ` (Versuch ${attempt})` : ''}`,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (authAutomat.state.status === 'loading') {
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(null, 'anon', { retryNotification: null })
|
||||
);
|
||||
syncSse(nextState);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
inFlightBootstrap = null;
|
||||
});
|
||||
|
||||
return inFlightBootstrap;
|
||||
},
|
||||
// #endregion
|
||||
|
||||
@@ -302,6 +351,7 @@ export const selectHasAuth = () => Boolean(authAutomat.state.hasAuth);
|
||||
export const selectHasCompany = () => Boolean(authAutomat.state.hasCompany);
|
||||
export const selectSelectedCompanyId = () =>
|
||||
authAutomat.state.hasCompany ? authAutomat.state.user?.selectedCompanyId ?? null : null;
|
||||
export const selectRetryNotification = () => authAutomat.state.retryNotification;
|
||||
|
||||
/**
|
||||
* Re-reads `GET /api/auth/me` from the server.
|
||||
@@ -312,7 +362,7 @@ export const selectSelectedCompanyId = () =>
|
||||
* @returns {Promise<{user: AuthUser|null, googleClientId: string|null}>} Session.
|
||||
*/
|
||||
export function bootstrapAuthMe() {
|
||||
return auth.me();
|
||||
return authAutomat.actions.bootstrapSession();
|
||||
}
|
||||
// #endregion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user