Files
gridpilot.gg/apps/website/lib/auth/LoginFlowController.ts
2026-01-14 02:02:24 +01:00

76 lines
2.2 KiB
TypeScript

/**
* Login Flow Controller
*
* Deterministic state machine for authentication flow.
* Compliant with docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md
*/
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
export enum LoginState {
UNAUTHENTICATED = "UNAUTHENTICATED",
AUTHENTICATED_WITH_PERMISSIONS = "AUTHENTICATED_WITH_PERMISSIONS",
AUTHENTICATED_WITHOUT_PERMISSIONS = "AUTHENTICATED_WITHOUT_PERMISSIONS",
POST_AUTH_REDIRECT = "POST_AUTH_REDIRECT"
}
export type LoginAction =
| { type: 'SHOW_LOGIN_FORM' }
| { type: 'REDIRECT'; path: string }
| { type: 'SHOW_PERMISSION_ERROR' };
/**
* LoginFlowController - Immutable, deterministic state machine
*
* Rules:
* - Constructed from explicit inputs only
* - No side effects
* - Pure functions for state transitions
* - Side effects (routing) executed outside
*/
export class LoginFlowController {
// Immutable state
private readonly session: SessionViewModel | null;
private readonly returnTo: string;
// State machine
private state: LoginState;
constructor(session: SessionViewModel | null, returnTo: string) {
this.session = session;
this.returnTo = returnTo;
this.state = this.determineInitialState();
}
private determineInitialState(): LoginState {
if (!this.session) return LoginState.UNAUTHENTICATED;
if (this.returnTo === '/dashboard') return LoginState.AUTHENTICATED_WITH_PERMISSIONS;
return LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS;
}
// Pure function - no side effects
getState(): LoginState {
return this.state;
}
// Pure function - returns action, does not execute
getNextAction(): LoginAction {
switch (this.state) {
case LoginState.UNAUTHENTICATED:
return { type: 'SHOW_LOGIN_FORM' };
case LoginState.AUTHENTICATED_WITH_PERMISSIONS:
return { type: 'REDIRECT', path: '/dashboard' };
case LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS:
return { type: 'SHOW_PERMISSION_ERROR' };
case LoginState.POST_AUTH_REDIRECT:
return { type: 'REDIRECT', path: this.returnTo };
}
}
// Transition called after authentication
transitionToPostAuth(): void {
if (this.session) {
this.state = LoginState.POST_AUTH_REDIRECT;
}
}
}