fix(panels): bypass GoTrueClient lock — expose accessToken via AuthContext, gate panels on auth

Root cause: apiFetch called supabase.auth.getSession() on every API request, acquiring
the GoTrueClient internal lock. On refresh, concurrent mount of panels caused lock
contention — the loadCabinets/loadSessions fetch awaits hung, loading spinner never
cleared.

- AuthContext: add accessToken state, set/clear it alongside user on all auth events
- CCFilesPanel: apiFetch reads accessToken from AuthContext (no lock), loadCabinets
  effect gated on authUser.id (fires only after SIGNED_IN, session guaranteed valid)
- CCTranscriptionPanel: loadSessions effect gated on authUser.id for same reason

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-25 15:06:51 +01:00
co-authored by Claude Sonnet 4.6
parent 5284d30f84
commit 6bd85671d2
3 changed files with 27 additions and 10 deletions
+9
View File
@@ -9,6 +9,7 @@ import { storageService, StorageKeys } from '../services/auth/localStorageServic
export interface AuthContextType {
user: CCUser | null;
user_role: string | null;
accessToken: string | null;
loading: boolean;
error: Error | null;
signIn: (email: string, password: string) => Promise<void>;
@@ -19,6 +20,7 @@ export interface AuthContextType {
export const AuthContext = createContext<AuthContextType>({
user: null,
user_role: null,
accessToken: null,
loading: true,
error: null,
signIn: async () => {},
@@ -30,6 +32,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const [user, setUser] = useState<CCUser | null>(null);
const [user_role, setUserRole] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true); // true until INITIAL_SESSION fires
const [error, setError] = useState<Error | null>(null);
@@ -84,15 +87,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const { user: resolvedUser, role } = await buildUserFromSupabase(session.user);
setUser(resolvedUser);
setUserRole(role);
setAccessToken(session.access_token ?? null);
} catch (buildError) {
logger.error('auth-context', '❌ Failed to build user from session', { event, error: buildError });
setUser(null);
setUserRole(null);
setAccessToken(null);
setError(buildError instanceof Error ? buildError : new Error('Failed to load user'));
}
} else {
setUser(null);
setUserRole(null);
setAccessToken(null);
}
// Always clear loading after the first auth event resolves
setLoading(false);
@@ -102,6 +108,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
persistSession(null);
setUser(null);
setUserRole(null);
setAccessToken(null);
setLoading(false);
break;
}
@@ -132,6 +139,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const { user: resolvedUser, role } = await buildUserFromSupabase(data.user);
setUser(resolvedUser);
setUserRole(role);
setAccessToken(data.session?.access_token ?? null);
}
} catch (error) {
logger.error('auth-context', '❌ Sign in failed', { error });
@@ -165,6 +173,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
value={{
user,
user_role,
accessToken,
loading,
error,
signIn,