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

6.6 KiB

authAutomat

Source File: 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

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

const initialState = {
  user: null,
  googleClientId: null,
  status: 'loading',
};

3. Configuration & Options

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

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:

// 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

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

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.