# Login Flow State Machine (Strict) This document defines the canonical, deterministic login flow controller for the website. Authoritative website contract: - [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1) ## 1) Core rule Login flow logic MUST be deterministic. The same inputs MUST produce the same state and the same next action. ## 2) State machine definition (strict) ### 2.1 State definitions ```typescript enum LoginState { UNAUTHENTICATED = "UNAUTHENTICATED", AUTHENTICATED_WITH_PERMISSIONS = "AUTHENTICATED_WITH_PERMISSIONS", AUTHENTICATED_WITHOUT_PERMISSIONS = "AUTHENTICATED_WITHOUT_PERMISSIONS", POST_AUTH_REDIRECT = "POST_AUTH_REDIRECT" } ``` ### 2.2 State transition table | Current State | Session | ReturnTo | Next State | Action | |---------------|---------|----------|------------|--------| | INITIAL | null | any | UNAUTHENTICATED | Show login form | | INITIAL | exists | '/dashboard' | AUTHENTICATED_WITH_PERMISSIONS | Redirect to dashboard | | INITIAL | exists | NOT '/dashboard' | AUTHENTICATED_WITHOUT_PERMISSIONS | Show permission error | | UNAUTHENTICATED | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo | | AUTHENTICATED_WITHOUT_PERMISSIONS | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo | ### 2.3 Class-based controller ```typescript class LoginFlowController { // Immutable state private readonly session: AuthSessionDTO | null; private readonly returnTo: string; // State machine private state: LoginState; constructor(session: AuthSessionDTO | 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; } } } ``` ## 3) Non-negotiable rules 1. The controller MUST be constructed from explicit inputs only. 2. The controller MUST NOT perform side effects. 3. Side effects (routing) MUST be executed outside the controller. 4. The controller MUST be unit-tested per transition. ## 4) Usage in login page (example) ```typescript export default function LoginPage() { const router = useRouter(); const searchParams = useSearchParams(); const { session } = useAuth(); const returnTo = searchParams.get('returnTo') ?? '/dashboard'; // Create controller once const controller = useMemo(() => new LoginFlowController(session, returnTo), [session, returnTo] ); // Get current state const state = controller.getState(); const action = controller.getNextAction(); // Execute action (only once) useEffect(() => { if (action.type === 'REDIRECT') { router.replace(action.path); } }, [action, router]); // Render based on state if (state === LoginState.UNAUTHENTICATED) { return ; } if (state === LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS) { return ; } // Show loading while redirecting return ; } ``` This pattern ensures deterministic behavior and makes the flow testable.