middleware fix wip
This commit is contained in:
132
docs/architecture/LOGIN_FLOW_STATE_MACHINE.md
Normal file
132
docs/architecture/LOGIN_FLOW_STATE_MACHINE.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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 <LoginForm />;
|
||||
}
|
||||
|
||||
if (state === LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS) {
|
||||
return <PermissionError returnTo={returnTo} />;
|
||||
}
|
||||
|
||||
// Show loading while redirecting
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
```
|
||||
|
||||
This eliminates all the unpredictable behavior and makes the flow testable and maintainable.
|
||||
Reference in New Issue
Block a user