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 (