-
Notifications
You must be signed in to change notification settings - Fork 112
fix: Fixes three critical race conditions that caused redirect loops … #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…after OAuth login:
1. Clear old token before processing OAuth callback to prevent axios
interceptor from attempting auto-refresh of expired old token while
processing new OAuth tokens
2. Add multi-stage authentication checks in AppAuthLayer with proper
timeouts to wait for React state sync before triggering logout
3. Add proactive state sync in AppConfig to ensure isAuthenticated
state matches localStorage token on mount
4. Create AppWorkspaceRedirect component to handle /app -> /app/:workspaceId
navigation after OAuth callback, preventing undefined workspace errors
Reviewer's GuideThis PR resolves multiple race conditions around OAuth authentication and redirect loops by enhancing HTTP service logging and token handling, implementing multi-stage auth state checks, proactively syncing auth status in AppConfig, and introducing a dedicated component to handle post-login workspace redirects, backed by a new Cypress test for the old-token scenario. Sequence diagram for OAuth login flow with race condition fixessequenceDiagram
actor User
participant "LoginAuth"
participant "signInWithUrl()"
participant "verifyToken()"
participant "refreshToken()"
participant "AppConfig"
participant "AppAuthLayer"
participant "AppWorkspaceRedirect"
User->>"LoginAuth": Complete OAuth, redirected to /auth/callback
"LoginAuth"->>"signInWithUrl()": Process callback URL
"signInWithUrl()"->>"signInWithUrl()": Clear old token from localStorage
"signInWithUrl()"->>"verifyToken()": Verify new access token
"signInWithUrl()"->>"refreshToken()": Refresh new token
"refreshToken()"->>"AppConfig": Save new token to localStorage
"AppConfig"->>"AppConfig": Proactively sync isAuthenticated state
"AppAuthLayer"->>"AppAuthLayer": Multi-stage authentication checks (timeouts)
"AppAuthLayer"->>"AppWorkspaceRedirect": Load workspace info
"AppWorkspaceRedirect"->>User: Redirect to /app/:workspaceId
Class diagram for new AppWorkspaceRedirect component and related hooksclassDiagram
class AppWorkspaceRedirect {
+useEffect()
+useNavigate()
+useUserWorkspaceInfo()
+LoadingDots
}
class useUserWorkspaceInfo {
<<hook>>
}
class LoadingDots {
}
AppWorkspaceRedirect --> useUserWorkspaceInfo
AppWorkspaceRedirect --> LoadingDots
Class diagram for updated AppAuthLayer authentication logicclassDiagram
class AppAuthLayer {
+useEffect() multi-stage auth check
+logout()
+context
+isAuthenticated
}
class AuthInternalContext {
}
AppAuthLayer --> AuthInternalContext
Class diagram for updated AppConfig authentication state syncclassDiagram
class AppConfig {
+useEffect() sync isAuthenticated on mount
+useEffect() sync isAuthenticated on change
+setIsAuthenticated()
+isAuthenticated
}
AppConfig --> setIsAuthenticated
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `src/components/app/layers/AppAuthLayer.tsx:105-107` </location>
<code_context>
+ // 1. Wait for React context to initialize (50ms)
+ // 2. Check token and auth state multiple times to handle async state updates
+ // 3. Only logout if consistently unauthenticated across multiple checks
+ let secondCheckTimeoutId: NodeJS.Timeout | null = null;
+
const timeoutId = setTimeout(() => {
</code_context>
<issue_to_address>
**suggestion:** Consider using window.setTimeout for browser compatibility.
NodeJS.Timeout is not compatible with browsers, which expect setTimeout to return a number. Use window.setTimeout and window.clearTimeout for cross-environment compatibility.
```suggestion
let secondCheckTimeoutId: number | null = null;
const timeoutId = window.setTimeout(() => {
```
</issue_to_address>
### Comment 2
<location> `src/components/app/AppWorkspaceRedirect.tsx:24-19` </location>
<code_context>
+
+ const workspaceId = userWorkspaceInfo.selectedWorkspace?.id;
+
+ if (!workspaceId) {
+ console.warn('[AppWorkspaceRedirect] No selected workspace found in user info', userWorkspaceInfo);
+ return;
+ }
+
</code_context>
<issue_to_address>
**suggestion:** No fallback for missing workspaceId may leave user stuck.
Consider adding a fallback UI or redirecting to an error page when workspaceId is missing to prevent users from being stuck on a loading screen.
Suggested implementation:
```typescript
if (!workspaceId) {
console.warn('[AppWorkspaceRedirect] No selected workspace found in user info', userWorkspaceInfo);
// Optionally, you can redirect to an error page:
// navigate('/app/error');
// Or set a local state to show a fallback UI
setShowWorkspaceError(true);
return;
}
```
```typescript
import { useState } from 'react';
export function AppWorkspaceRedirect() {
const userWorkspaceInfo = useUserWorkspaceInfo();
const [showWorkspaceError, setShowWorkspaceError] = useState(false);
```
```typescript
const userWorkspaceInfo = useUserWorkspaceInfo();
if (showWorkspaceError) {
return (
<div style={{ textAlign: 'center', marginTop: '2rem' }}>
<h2>Workspace Not Found</h2>
<p>
We couldn't find a workspace for your account. Please check your account or contact support.
</p>
<button onClick={() => navigate('/app')}>Go to Home</button>
</div>
);
}
```
</issue_to_address>
### Comment 3
<location> `src/components/main/AppConfig.tsx:90` </location>
<code_context>
+ const userWorkspaceInfo = useUserWorkspaceInfo();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (!userWorkspaceInfo) {
+ console.debug('[AppWorkspaceRedirect] Waiting for workspace info to load...');
</code_context>
<issue_to_address>
**issue (complexity):** Consider consolidating multiple authentication sync effects into a single effect or custom hook to reduce duplication.
You can collapse all of the “sync on mount”, “sync on isAuthenticated change”, “storage” and “SESSION_INVALID” effects into one (or a small custom hook) to eliminate duplication.
For example, in‐place you might do:
```ts
useEffect(() => {
const syncAuth = () => {
const hasToken = isTokenValid();
console.debug('[AppConfig] sync', {hasToken, isAuthenticated});
if (hasToken && !isAuthenticated) {
setIsAuthenticated(true);
} else if (!hasToken && isAuthenticated) {
setIsAuthenticated(false);
}
};
// immediate + delayed mount sync
syncAuth();
const timeoutId = setTimeout(syncAuth, 100);
// storage listener
const onStorage = (e: StorageEvent) => e.key === 'token' && syncAuth();
window.addEventListener('storage', onStorage);
// session‐invalid listener
const offInvalid = on(EventType.SESSION_INVALID, () => setIsAuthenticated(false));
return () => {
clearTimeout(timeoutId);
window.removeEventListener('storage', onStorage);
offInvalid();
};
}, [isAuthenticated]);
```
Or extract it into a `useAuthSync` hook:
```ts
function useAuthSync(
isAuthenticated: boolean,
setIsAuthenticated: (v: boolean) => void
) {
useEffect(() => {
// …same body as above…
}, [isAuthenticated]);
}
// in AppConfig.tsx
const [isAuthenticated, setIsAuthenticated] = useState(isTokenValid());
useAuthSync(isAuthenticated, setIsAuthenticated);
```
This eliminates three separate effects plus duplicated logic but keeps exactly the same behavior.
</issue_to_address>
### Comment 4
<location> `cypress/e2e/auth/oauth-login.cy.ts:516` </location>
<code_context>
const body = req.body;
</code_context>
<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))
```suggestion
const {body} = req;
```
<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.
From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
fix: Fixes three critical race conditions that caused redirect loops …
Fixes three critical race conditions that caused redirect loops after OAuth login:
Clear old token before processing OAuth callback to prevent axios
interceptor from attempting auto-refresh of expired old token while
processing new OAuth tokens
Add multi-stage authentication checks in AppAuthLayer with proper
timeouts to wait for React state sync before triggering logout
Add proactive state sync in AppConfig to ensure isAuthenticated
state matches localStorage token on mount
Create AppWorkspaceRedirect component to handle /app -> /app/:workspaceId
navigation after OAuth callback, preventing undefined workspace errors
Summary by Sourcery
Fix race conditions in the OAuth login flow by clearing old tokens before callback processing, adding staged auth checks and proactive state sync to prevent redirect loops, and redirecting /app to the proper workspace route.
New Features:
Bug Fixes:
Enhancements:
Tests: