205 lines
6.6 KiB
Markdown
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.
|