From f59ab9d4dd7b680b99f30b4a61e6ee64a199a6c8 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 4 Jun 2026 04:17:31 -0700 Subject: [PATCH] [eric] auth: drop the mandatory first-launch sign-in gate, app works signed-out --- backend/apps/auth/router.py | 62 +------------- backend/tests/test_auth_router.py | 84 ------------------- frontend/src/app/Main.tsx | 32 +------ .../components/Onboarding/OnboardingRoot.tsx | 4 +- 4 files changed, 5 insertions(+), 177 deletions(-) diff --git a/backend/apps/auth/router.py b/backend/apps/auth/router.py index c069e33a..5fb72b50 100644 --- a/backend/apps/auth/router.py +++ b/backend/apps/auth/router.py @@ -12,12 +12,7 @@ POST /api/auth/signin-activate {token, signin_method, email?} POST /api/auth/signout Calls cloud /api/auth/signout to revoke the bearer, then clears local - identity fields. Brings the user back to the sign-in gate. - -POST /api/auth/identity-status {install_id?} - Local proxy to cloud /api/me/identity-status; drives the gate's - soft-vs-hard decision. Wraps it in our local backend so the renderer - doesn't need to know the cloud URL. + identity fields. """ from __future__ import annotations @@ -90,7 +85,7 @@ async def signin_activate(body: SigninActivateRequest): POSTs to this endpoint after a Google OAuth or magic-link flow. We re-validate the bearer with the cloud; never just trust whatever arrives at the localhost endpoint; then write user_id + email + - signin_method to settings so the renderer can dismiss the gate. + signin_method to settings so the renderer flips to signed-in. """ if not body.token or len(body.token) < 16: raise HTTPException(status_code=400, detail="Invalid token") @@ -240,56 +235,3 @@ async def signout(): await save_settings_async(settings_obj) _sync_identity_to_service(settings_obj) return {"ok": True} - - -# --------------------------------------------------------------------------- -# GET /api/auth/identity-status -# --------------------------------------------------------------------------- - -@auth.router.get("/identity-status") -async def identity_status(): - """Returns gate-state for the renderer. - - The renderer's SignInGateLoader calls this on mount to decide between - soft gate (banner) vs hard gate (modal). Local-side authoritative - field is settings.user_id; the cloud answers install age + grace - deadline. - """ - settings_obj = load_settings() - user_id = getattr(settings_obj, "user_id", None) - if user_id: - return { - "authed": True, - "user_id": user_id, - "email": getattr(settings_obj, "user_email", None), - "signin_method": getattr(settings_obj, "signin_method", None), - "hard_gate": False, - } - - # Not signed in; defer to cloud for install-age + grace-window math. - install_id = getattr(settings_obj, "installation_id", None) - if not install_id: - # No install_id yet (very fresh install before first sync); hard gate. - return {"authed": False, "hard_gate": True, "install_age_days": 0, "deadline_ts": None} - - proxy = _proxy_url() - try: - async with httpx.AsyncClient(timeout=5.0) as client: - r = await client.get( - f"{proxy}/api/me/identity-status", - params={"install_id": install_id}, - ) - if r.status_code == 200: - data = r.json() - return { - "authed": False, - "hard_gate": bool(data.get("hard_gate", True)), - "install_age_days": int(data.get("install_age_days", 0)), - "deadline_ts": data.get("deadline_ts"), - } - except httpx.HTTPError as e: - logger.debug("identity-status cloud fetch failed: %s", e) - - # Cloud unreachable; fail open with soft gate so a flaky network - # doesn't lock the user out. Renderer will retry on next mount. - return {"authed": False, "hard_gate": False, "install_age_days": 0, "deadline_ts": None} diff --git a/backend/tests/test_auth_router.py b/backend/tests/test_auth_router.py index 2fe86d15..76c6891f 100644 --- a/backend/tests/test_auth_router.py +++ b/backend/tests/test_auth_router.py @@ -135,90 +135,6 @@ def test_signin_activate_short_token_rejected_locally(client, reset_settings): assert r.status_code == 400 -# --------------------------------------------------------------------------- -# /api/auth/identity-status, gate-state for the renderer -# --------------------------------------------------------------------------- - -def test_identity_status_signed_in_user_returns_authed_true(client, reset_settings): - from backend.apps.settings.settings import load_settings, _save_settings - s = load_settings() - s.user_id = "u-already-signed-in" - s.user_email = "in@example.com" - s.signin_method = "google" - _save_settings(s) - - r = client.get("/api/auth/identity-status") - assert r.status_code == 200 - body = r.json() - assert body["authed"] is True - assert body["user_id"] == "u-already-signed-in" - assert body["hard_gate"] is False - - -def test_identity_status_unsigned_no_install_id_hard_gates(client, reset_settings): - from backend.apps.settings.settings import load_settings, _save_settings - s = load_settings() - s.user_id = None - s.user_email = None - s.signin_method = None - s.installation_id = None - _save_settings(s) - - r = client.get("/api/auth/identity-status") - assert r.status_code == 200 - body = r.json() - assert body["authed"] is False - assert body["hard_gate"] is True - - -def test_identity_status_cloud_unreachable_fails_open_to_soft(client, reset_settings): - """If the cloud is unreachable, fall back to soft gate so the user - isn't locked out by a flaky network. Renderer retries on next mount.""" - from backend.apps.settings.settings import load_settings, _save_settings - s = load_settings() - s.user_id = None - s.installation_id = "test-install-aaa" - _save_settings(s) - - with patch("httpx.AsyncClient") as MockClient: - instance = MockClient.return_value.__aenter__.return_value - # Simulate network error. - import httpx as _httpx - instance.get = AsyncMock(side_effect=_httpx.HTTPError("network down")) - - r = client.get("/api/auth/identity-status") - assert r.status_code == 200 - body = r.json() - assert body["authed"] is False - assert body["hard_gate"] is False # fail open - - -def test_identity_status_cloud_says_hard_gate(client, reset_settings): - from backend.apps.settings.settings import load_settings, _save_settings - s = load_settings() - s.user_id = None - s.installation_id = "test-install-bbb" - _save_settings(s) - - fake_response = AsyncMock() - fake_response.status_code = 200 - fake_response.json = lambda: { - "authed": False, - "hard_gate": True, - "install_age_days": 60, - "deadline_ts": 1000, - } - with patch("httpx.AsyncClient") as MockClient: - instance = MockClient.return_value.__aenter__.return_value - instance.get = AsyncMock(return_value=fake_response) - - r = client.get("/api/auth/identity-status") - body = r.json() - assert body["authed"] is False - assert body["hard_gate"] is True - assert body["install_age_days"] == 60 - - # --------------------------------------------------------------------------- # /api/auth/signout # --------------------------------------------------------------------------- diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx index d7fa1be6..34a29123 100644 --- a/frontend/src/app/Main.tsx +++ b/frontend/src/app/Main.tsx @@ -33,7 +33,6 @@ const Analytics = React.lazy(() => import('./pages/Analytics/Analytics')); const OnboardingRoot = React.lazy(() => import('./components/Onboarding').then((m) => ({ default: m.OnboardingRoot })), ); -const SignInGate = React.lazy(() => import('./components/overlays/SignInGate')); if (typeof window !== 'undefined') { // Diagnostic global error capture. The packaged bundle has no source maps, so without these handlers the only thing that reaches main-process stderr is "Uncaught TypeError: ... (bundle.js:2)" with zero stack context. Forward error.stack and Redux action.type when available so we can pinpoint the offender across the chat-spawn / workflow rendering paths even in minified prod. @@ -250,36 +249,11 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) = if (!loaded) return; (window as any).openswarm?.setAllowPrerelease?.(allowExperimentalUpdates); }, [loaded, allowExperimentalUpdates]); + // Hold paint until settings land so the user's theme renders first; Electron's ready-to-show relies on this. + if (!loaded) return null; return <>{children}; }; -/** Mandatory sign-in gate; first thing shown when settings lack a user_id or bearer. */ -const SignInGateLoader: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const dispatch = useAppDispatch(); - const settings = useAppSelector((s) => s.settings.data); - const settingsLoaded = useAppSelector((s) => s.settings.loaded); - - const alreadySignedIn = Boolean(settings.user_id || settings.openswarm_bearer_token); - - useEffect(() => { - if (!settingsLoaded || alreadySignedIn) return; - const id = setInterval(() => { dispatch(fetchSettings()); }, 2000); - return () => clearInterval(id); - }, [dispatch, settingsLoaded, alreadySignedIn]); - - if (!settingsLoaded) return null; - if (alreadySignedIn) return <>{children}; - - return ( - <> - {children} - - - - - ); -}; - const DEFAULT_MODEL_PRIORITY: string[] = [ 'Anthropic', 'OpenAI', @@ -488,7 +462,6 @@ const ThemedApp: React.FC = () => { - @@ -519,7 +492,6 @@ const ThemedApp: React.FC = () => { - diff --git a/frontend/src/app/components/Onboarding/OnboardingRoot.tsx b/frontend/src/app/components/Onboarding/OnboardingRoot.tsx index c528757e..ac309590 100644 --- a/frontend/src/app/components/Onboarding/OnboardingRoot.tsx +++ b/frontend/src/app/components/Onboarding/OnboardingRoot.tsx @@ -29,7 +29,6 @@ const OnboardingRoot: React.FC = () => { const store = useStore() as Store; const tokens = useClaudeTokens(); const progress = useAppSelector((s) => s.onboardingProgress); - const userId = useAppSelector((s) => s.settings.data.user_id ?? null); const settingsLoaded = useAppSelector((s) => s.settings.loaded); useEffect(() => { @@ -210,8 +209,7 @@ const OnboardingRoot: React.FC = () => { return () => onboardingDirector.detach(); }, [store, tokens.accent.primary]); - // Wait for sign-in state so we don't render under the SignInGate's z-index. - if (!settingsLoaded || !userId) return null; + if (!settingsLoaded) return null; if (!progress.initialized) return null; return (