automat
This commit is contained in:
180
src/automats/authAutomat.js
Normal file
180
src/automats/authAutomat.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Automat } from 'react-automat';
|
||||
import { auth, company } from '../api';
|
||||
|
||||
/**
|
||||
* @typedef {Object} AuthUser
|
||||
* @property {string|number|null} [selectedCompanyId] Currently selected company.
|
||||
* @property {*} [key] Additional user properties returned by the API.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AuthState
|
||||
* @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions.
|
||||
* @property {string|null} googleClientId Google Identity Services client ID.
|
||||
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
|
||||
*/
|
||||
|
||||
/** Emitted after an authenticated session has been established. */
|
||||
export const AUTH_SESSION_ESTABLISHED = 'auth/sessionEstablished';
|
||||
/** Emitted after the active company has changed. */
|
||||
export const AUTH_COMPANY_CHANGED = 'auth/companyChanged';
|
||||
/** Emitted after the current user has logged out. */
|
||||
export const AUTH_LOGGED_OUT = 'auth/loggedOut';
|
||||
|
||||
/** @type {AuthState} */
|
||||
const initialState = {
|
||||
user: null,
|
||||
googleClientId: null,
|
||||
status: 'loading',
|
||||
};
|
||||
|
||||
let emit = () => { };
|
||||
|
||||
/**
|
||||
* Registers the application-level dispatcher used for authentication events.
|
||||
*
|
||||
* @param {(event: {type: string}) => void} dispatch Event dispatcher.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setAuthEventDispatcher(dispatch) {
|
||||
emit = dispatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges API user data while retaining the current company selection when the
|
||||
* response does not contain one.
|
||||
*
|
||||
* @param {AuthUser|null} current Current user.
|
||||
* @param {AuthUser|null|undefined} incoming User data returned by the API.
|
||||
* @returns {AuthUser|null} Merged user.
|
||||
*/
|
||||
function mergeUser(current, incoming) {
|
||||
if (!incoming) return current;
|
||||
const selectedCompanyId =
|
||||
incoming.selectedCompanyId ?? current?.selectedCompanyId ?? null;
|
||||
return { ...current, ...incoming, selectedCompanyId };
|
||||
}
|
||||
|
||||
let authAutomat;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Restores an existing session or requests it from the authentication API.
|
||||
*
|
||||
* @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') {
|
||||
authAutomat.setState({ user: null, googleClientId, status: 'anon' });
|
||||
} else {
|
||||
authAutomat.setState({ googleClientId });
|
||||
}
|
||||
return nextSession;
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, nextSession.user);
|
||||
authAutomat.setState({ user, googleClientId, status: 'authed' });
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return nextSession;
|
||||
} catch (error) {
|
||||
if (authAutomat.state.status === 'loading') {
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Exchanges a Google credential for an authenticated application session.
|
||||
*
|
||||
* @param {string} credential Google Identity Services credential.
|
||||
* @returns {Promise<Object>} Authenticated session.
|
||||
*/
|
||||
async loginWithGoogle(credential) {
|
||||
try {
|
||||
const { session } = await auth.loginWithGoogle(credential);
|
||||
if (!session?.user) {
|
||||
throw new Error('Login ok but session cookie not accepted (/api/auth/me still unauthorized)');
|
||||
}
|
||||
authAutomat.setState({
|
||||
user: session.user,
|
||||
googleClientId: session.googleClientId ?? authAutomat.state.googleClientId,
|
||||
status: 'authed',
|
||||
});
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return session;
|
||||
} catch (error) {
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects the company used by the current session.
|
||||
*
|
||||
* @param {string|number} companyId Company identifier.
|
||||
* @returns {Promise<AuthUser>} Updated user.
|
||||
*/
|
||||
async selectCompany(companyId) {
|
||||
const data = await company.select(companyId);
|
||||
const user = mergeUser(
|
||||
authAutomat.state.user,
|
||||
data.user ?? { selectedCompanyId: companyId },
|
||||
);
|
||||
authAutomat.setState({ user, status: 'authed' });
|
||||
emit({ type: AUTH_COMPANY_CHANGED });
|
||||
return user;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a company and selects it for the current user.
|
||||
*
|
||||
* @param {Object} payload Company creation payload.
|
||||
* @returns {Promise<AuthUser>} Updated user.
|
||||
*/
|
||||
async createCompany(payload) {
|
||||
const data = await company.create(payload);
|
||||
let incoming = data.user;
|
||||
if (!incoming && data.company?.id) {
|
||||
const selected = await company.select(data.company.id);
|
||||
incoming = selected.user ?? { selectedCompanyId: data.company.id };
|
||||
}
|
||||
if (!incoming) {
|
||||
throw new Error('Unternehmen angelegt, aber keine Benutzerdaten erhalten.');
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, incoming);
|
||||
authAutomat.setState({ user, status: 'authed' });
|
||||
emit({ type: AUTH_COMPANY_CHANGED });
|
||||
return user;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ends the server session, clears the current user, and returns to `/`.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logout() {
|
||||
await auth.logout();
|
||||
window.history.replaceState(null, '', '/');
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
emit({ type: AUTH_LOGGED_OUT });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Global authentication state container.
|
||||
*
|
||||
* @type {Automat<AuthState>}
|
||||
*/
|
||||
authAutomat = new Automat(initialState, actions, { name: 'auth' });
|
||||
|
||||
export default authAutomat;
|
||||
Reference in New Issue
Block a user