u
This commit is contained in:
204
docs/authAutomat.md
Normal file
204
docs/authAutomat.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user