# Login Flow State Machine Architecture ## Problem The current login page has unpredictable behavior due to: - Multiple useEffect runs with different session states - Race conditions between session loading and redirect logic - Client-side redirects that interfere with test expectations ## Solution: State Machine Pattern ### 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" } ``` ### 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 | ### 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, doesn't 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 }; } } // Called after authentication transitionToPostAuth(): void { if (this.session) { this.state = LoginState.POST_AUTH_REDIRECT; } } } ``` ### Benefits 1. **Predictable**: Same inputs always produce same outputs 2. **Testable**: Can test each state transition independently 3. **No Race Conditions**: State determined once at construction 4. **Clear Intent**: Each state has a single purpose 5. **Maintainable**: Easy to add new states or modify transitions ### Usage in Login Page ```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 eliminates all the unpredictable behavior and makes the flow testable and maintainable.