middleware fix wip

This commit is contained in:
2026-01-04 12:49:30 +01:00
parent 729d95cd73
commit 691e6e2c7e
10 changed files with 741 additions and 152 deletions

View 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.