Files
dashfront/docs/authAutomat.md
2026-09-23 17:24:33 +02:00

205 lines
6.6 KiB
Markdown

# `authAutomat`
> **Source File**: [`src/automats/authAutomat.js`](file:///home/seb/src/dashfront/src/automats/authAutomat.js)
> **Registration Key**: `'auth'` (`Automat.get('auth')` / `window.__AUTOMATS__.get('auth')`)
---
## 1. Overview & Purpose
`authAutomat` manages the authentication and session lifecycle of the Dashfront client, as well as tenant company selection. It acts as the root coordinator for all other configuration and catalog automats (`companyAutomat`, `categoryAutomat`, `brandAutomat`, `unitAutomat`, `posterminalAutomat`, `supplierAutomat`).
### Core Responsibilities
- **Session Lifecycle**: Handles transitioning through `'loading'` → `'authed'` or `'anon'`.
- **Google One Tap / GIS Integration**: Exchanging Google OAuth JWTs for session cookies via `/api/auth/google`.
- **Multi-Tenant Company Switching**: Updating the active company context via `/api/auth/selected-company` and synchronizing user state.
- **Event Dispatching**: Emits cross-subsystem events for session establishment, company changes, and logout.
- **Automatic Sub-Store Bootstrap**: Downstream configuration automats subscribe to `authAutomat` to load or clear themselves reactively.
---
## 2. State Structure
```ts
interface AuthUser {
id?: string | number;
googleAuthId?: string;
email?: string;
name?: string;
picture?: string | null;
selectedCompanyId?: string | number | null;
companies?: Array<{ id: string | number; name: string }>;
[key: string]: any;
}
interface AuthState {
/** Authenticated user profile, or null when anonymous. */
user: AuthUser | null;
/** Google OAuth Client ID provided by backend bootstrap. */
googleClientId: string | null;
/** Current authentication lifecycle phase. */
status: 'loading' | 'anon' | 'authed';
}
```
### Initial State
```js
const initialState = {
user: null,
googleClientId: null,
status: 'loading',
};
```
---
## 3. Configuration & Options
```js
new Automat(initialState, actions, { name: 'auth' });
```
- **`name`**: `'auth'`
- **`persist`**: `false` (session token resides in secure HTTP-only cookies).
---
## 4. Lifecycle Events & Dispatcher
Constants exported from `authAutomat`:
- **`AUTH_SESSION_ESTABLISHED = 'auth/sessionEstablished'`**: Fired when a valid session is recognized.
- **`AUTH_COMPANY_CHANGED = 'auth/companyChanged'`**: Fired when the active company changes or a new company is created.
- **`AUTH_LOGGED_OUT = 'auth/loggedOut'`**: Fired when user logs out.
### Event Dispatcher Registration
```js
import { setAuthEventDispatcher } from '../automats/authAutomat';
// Allows routing events to an external system, logger, or telemetry
setAuthEventDispatcher((event) => {
console.log('[Auth Event]', event.type);
});
```
---
## 5. Actions & Methods
Access actions via `authAutomat.actions.<method>()`.
### `bootstrapSession(session?: object): Promise<object>`
Initializes the session on application startup:
1. If `session` is passed directly, uses it; otherwise fetches `GET /api/auth/me`.
2. Updates `googleClientId`.
3. If no user is returned, sets status to `'anon'`.
4. If a user is returned, merges the profile, normalizes base64 avatar pictures, sets status to `'authed'`, and emits `AUTH_SESSION_ESTABLISHED`.
### `loginWithGoogle(credential: string): Promise<object>`
Submits a Google One Tap JWT to `POST /api/auth/google`. On success, updates user state, sets status to `'authed'`, and emits `AUTH_SESSION_ESTABLISHED`.
### `selectCompany(companyId: string | number): Promise<AuthUser>`
Calls `PUT /api/auth/selected-company` to switch active company context in the backend session cookie. Updates `state.user.selectedCompanyId`, triggers `AUTH_COMPANY_CHANGED`, and returns the updated user.
### `createCompany(payload: object): Promise<AuthUser>`
Creates a new tenant company via `POST /api/company` and automatically selects it for the session. Emits `AUTH_COMPANY_CHANGED`.
### `logout(): Promise<void>`
Calls `POST /api/auth/logout`, replaces history state to `'/'`, clears user state, sets status to `'anon'`, and emits `AUTH_LOGGED_OUT`.
---
## 6. Cross-Automat Coordination
Downstream automats coordinate with `authAutomat` at module load time. For example:
```js
// In companyAutomat.js / brandAutomat.js / unitAutomat.js / posterminalAutomat.js:
authAutomat.subscribe((auth) => {
const companyId = auth.user?.selectedCompanyId;
if (auth.status === 'authed' && companyId) {
void targetAutomat.actions.load();
} else if (auth.status === 'anon') {
targetAutomat.actions.reset();
}
});
```
When `auth.status` transitions to `'authed'` with a `selectedCompanyId`, all dependent stores load their data. On logout (`'anon'`), they clear their state.
---
## 7. Usage Examples
### A. React `PureComponent` Login Gate / User Menu
```jsx
import { PureComponent } from 'react';
import authAutomat from '../automats/authAutomat';
export class UserMenu extends PureComponent {
constructor(props) {
super(props);
this.state = {
user: authAutomat.state.user,
status: authAutomat.state.status,
};
}
componentDidMount() {
this.unsub = authAutomat.subscribe(this, ['user', 'status']);
}
componentWillUnmount() {
this.unsub?.();
}
handleCompanyChange = (e) => {
authAutomat.actions.selectCompany(e.target.value);
};
handleLogout = () => {
authAutomat.actions.logout();
};
render() {
const { user, status } = this.state;
if (status === 'loading') return <div>Authentifizierung...</div>;
if (status === 'anon' || !user) return <div>Nicht angemeldet</div>;
return (
<div>
<span>{user.name} ({user.email})</span>
<select value={user.selectedCompanyId ?? ''} onChange={this.handleCompanyChange}>
{user.companies?.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<button onClick={this.handleLogout}>Abmelden</button>
</div>
);
}
}
```
### B. Application Bootstrap in `AuthBootstrap.jsx`
```js
import authAutomat from '../automats/authAutomat';
async function initApp() {
try {
await authAutomat.actions.bootstrapSession();
} catch (err) {
console.error('Session bootstrap failed:', err);
}
}
```
---
## 8. Best Practices & Gotchas
1. **Never pass `googleAuthId`**: The frontend must never send `googleAuthId` in request bodies or query params. Identity is extracted from the session cookie on the server.
2. **Handle `'loading'` State**: Do not render authentication-required views while `authAutomat.state.status === 'loading'`.
3. **Picture normalization**: `authAutomat` automatically formats raw base64 picture strings into valid `data:image/...;base64,...` URIs.