mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-25 14:02:22 +02:00
[eric] sign-in flow polish: localhost activate + auto-refresh + clean signout
This commit is contained in:
@@ -168,7 +168,14 @@ async def signin_activate(body: SigninActivateRequest):
|
||||
|
||||
@auth.router.post("/signout")
|
||||
async def signout():
|
||||
"""Revoke the cloud-side bearer + clear local identity state."""
|
||||
"""Revoke the cloud-side bearer + clear local identity state.
|
||||
|
||||
Also stops every in-flight agent session so any 9Router subprocess
|
||||
that captured the now-revoked bearer at spawn time can't keep using
|
||||
it. Without this, a signed-out user's old chat tabs would keep
|
||||
making /v1/messages calls with a token the cloud has revoked,
|
||||
surfacing as 401s in the agent UI ("Invalid bearer token").
|
||||
"""
|
||||
settings_obj = load_settings()
|
||||
bearer = getattr(settings_obj, "openswarm_bearer_token", None)
|
||||
proxy = _proxy_url()
|
||||
@@ -184,6 +191,25 @@ async def signout():
|
||||
# the cloud token is invalidated lazily on next use anyway.
|
||||
logger.warning("cloud signout failed (clearing local anyway): %s", e)
|
||||
|
||||
# Stop every running agent session BEFORE clearing local state. The
|
||||
# next message in any tab will then re-launch the session, which
|
||||
# re-reads settings (now empty) and re-spawns 9Router subprocesses
|
||||
# with whatever the user re-signs-in with — or with own_key mode
|
||||
# if they don't sign back in. Best-effort: failures here shouldn't
|
||||
# block the sign-out itself.
|
||||
try:
|
||||
from backend.apps.agents.agent_manager import agent_manager
|
||||
running = list(agent_manager.tasks.keys())
|
||||
for session_id in running:
|
||||
try:
|
||||
await agent_manager.stop_agent(session_id)
|
||||
except Exception as e:
|
||||
logger.warning("signout: stop_agent(%s) failed: %s", session_id, e)
|
||||
if running:
|
||||
logger.info("signout: stopped %d in-flight agent session(s)", len(running))
|
||||
except Exception as e:
|
||||
logger.warning("signout: agent shutdown skipped: %s", e)
|
||||
|
||||
settings_obj.user_id = None
|
||||
settings_obj.user_email = None
|
||||
settings_obj.signin_method = None
|
||||
|
||||
@@ -99,6 +99,14 @@ _AUTH_EXEMPT_EXACT = {
|
||||
# has no way to inject our bearer token; the install_id check inside
|
||||
# the handler is what binds the request to this user.
|
||||
"/api/tools/oauth/cloud-claim",
|
||||
# Bearer-handoff endpoints called by api.openswarm.com's success page
|
||||
# AFTER Stripe checkout / Google sign-in / magic-link sign-in. The
|
||||
# request POSTs the just-minted cloud bearer; the handler then re-
|
||||
# validates it against the cloud (/api/me or /api/auth/signin-activate).
|
||||
# The browser has no way to attach our per-install token here — the
|
||||
# cloud-validated bearer in the body is the actual auth mechanism.
|
||||
"/api/subscription/activate",
|
||||
"/api/auth/signin-activate",
|
||||
"/api/version",
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,19 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) =
|
||||
})
|
||||
.catch(() => { /* offline — next launch will reconcile */ });
|
||||
}, [dispatch]);
|
||||
|
||||
// Refetch settings when the window regains focus. Catches every out-of-
|
||||
// band settings mutation that doesn't come through a renderer-dispatched
|
||||
// thunk: Stripe checkout's bearer-handoff page POSTing /api/subscription/
|
||||
// activate, the new sign-in flow's bearer-handoff POSTing /api/auth/
|
||||
// signin-activate, manual ~/.openswarm/settings.json edits, etc. Throttled
|
||||
// by the browser's natural focus cadence (one refetch per Cmd-Tab back).
|
||||
useEffect(() => {
|
||||
const onFocus = () => { dispatch(fetchSettings()); };
|
||||
window.addEventListener('focus', onFocus);
|
||||
return () => window.removeEventListener('focus', onFocus);
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) setThemeMode(theme as 'light' | 'dark');
|
||||
}, [loaded, theme, setThemeMode]);
|
||||
@@ -220,6 +233,7 @@ interface IdentityStatus {
|
||||
}
|
||||
|
||||
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 [status, setStatus] = useState<IdentityStatus | null>(null);
|
||||
@@ -249,6 +263,19 @@ const SignInGateLoader: React.FC<{ children: React.ReactNode }> = ({ children })
|
||||
return () => { cancelled = true; };
|
||||
}, [settingsLoaded, alreadySignedIn]);
|
||||
|
||||
// While the gate is showing, poll settings every 2s so the moment the
|
||||
// sign-in flow completes (browser POSTs /api/auth/signin-activate, local
|
||||
// backend persists user_id to settings.json), we re-read settings and
|
||||
// the gate auto-dismisses without the user clicking anything. Cheap —
|
||||
// /api/settings is a static file read on the local backend. Stops as
|
||||
// soon as the user is authed or the user has skipped.
|
||||
useEffect(() => {
|
||||
if (!settingsLoaded || alreadySignedIn) return;
|
||||
if (status?.authed) return;
|
||||
const id = setInterval(() => { dispatch(fetchSettings()); }, 2000);
|
||||
return () => clearInterval(id);
|
||||
}, [dispatch, settingsLoaded, alreadySignedIn, status?.authed]);
|
||||
|
||||
// Read the persisted "remind me later" timestamp from localStorage so
|
||||
// soft-gate skip survives reloads but doesn't bloat AppSettings.
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user