mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-02 14:28:59 +02:00
[eric] auth: every user signs in, including upgraders who skipped onboarding entirely
This commit is contained in:
@@ -29,6 +29,7 @@ import { setPanelMode, disableOnboardingAfterCrash } from '@/shared/state/onboar
|
||||
|
||||
const Analytics = React.lazy(() => import('./pages/Analytics/Analytics'));
|
||||
const OnboardingV3Root = React.lazy(() => import('./components/OnboardingV3/OnboardingV3Root'));
|
||||
const SignInRequiredGate = React.lazy(() => import('./components/overlays/SignInRequiredGate'));
|
||||
const OnboardingRoot = React.lazy(() =>
|
||||
import('./components/Onboarding').then((m) => ({ default: m.OnboardingRoot })),
|
||||
);
|
||||
@@ -547,6 +548,9 @@ const ThemedApp: React.FC = () => {
|
||||
<OnboardingV3Root />
|
||||
</Suspense>
|
||||
</OnboardingErrorGuard>
|
||||
<Suspense fallback={null}>
|
||||
<SignInRequiredGate />
|
||||
</Suspense>
|
||||
</DeepLinkListener>
|
||||
</UpdateListener>
|
||||
</DefaultModelGuard>
|
||||
|
||||
@@ -24,7 +24,7 @@ type Stage = 'choose' | 'email_form' | 'code_form';
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
||||
|
||||
export default function SignInDialog({ onClose, initialStage = 'choose' }: { onClose: () => void; initialStage?: Stage }): JSX.Element {
|
||||
export default function SignInDialog({ onClose, initialStage = 'choose', mandatory = false }: { onClose: () => void; initialStage?: Stage; mandatory?: boolean }): JSX.Element {
|
||||
const tokens = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const proxyUrl = useAppSelector(
|
||||
@@ -156,7 +156,9 @@ export default function SignInDialog({ onClose, initialStage = 'choose' }: { onC
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onClose={onClose}
|
||||
// A mandatory sign-in has no way out on purpose, so Esc and a backdrop click must not be one.
|
||||
onClose={mandatory ? undefined : onClose}
|
||||
disableEscapeKeyDown={mandatory}
|
||||
hideBackdrop={false}
|
||||
// Must clear the onboarding curtain (z ~2147483000): at MUI's default 1300 this dialog opened
|
||||
// INVISIBLY behind it, so "Continue with email" looked dead during onboarding.
|
||||
@@ -178,14 +180,16 @@ export default function SignInDialog({ onClose, initialStage = 'choose' }: { onC
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
sx={{ position: 'absolute', top: 10, right: 10, color: tokens.text.tertiary }}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
{!mandatory && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
sx={{ position: 'absolute', top: 10, right: 10, color: tokens.text.tertiary }}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{stage === 'code_form' ? (
|
||||
<>
|
||||
<Typography
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Everyone gets an account, upgraders included.
|
||||
//
|
||||
// A fresh install signs in inside onboarding (BeatSignIn's Continue is disabled until it lands).
|
||||
// A veteran never saw that: OnboardingV3Root writes `onboarding_v3: 'skipped'` the moment it finds
|
||||
// v2 history, so the whole flow, sign-in included, is skipped and the user runs unsigned forever.
|
||||
// That is the hole this closes, and it is why the gate lives here rather than inside onboarding.
|
||||
import React from 'react';
|
||||
|
||||
import SignInDialog from '@/app/components/overlays/SignInDialog';
|
||||
import { shouldRequireSignIn } from '@/app/components/overlays/shouldRequireSignIn';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
|
||||
export default function SignInRequiredGate(): JSX.Element | null {
|
||||
const settingsLoaded = useAppSelector((s) => s.settings.loaded);
|
||||
const userId = useAppSelector((s) => s.settings.data.user_id ?? null);
|
||||
const onboardingActive = useAppSelector((s) => s.onboardingV3.flowActive);
|
||||
|
||||
if (!shouldRequireSignIn({ settingsLoaded, userId, onboardingActive })) return null;
|
||||
|
||||
return <SignInDialog mandatory onClose={() => undefined} />;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Run: node --test frontend/src/app/components/overlays/shouldRequireSignIn.test.ts
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { shouldRequireSignIn } from './shouldRequireSignIn.ts';
|
||||
|
||||
const base = { settingsLoaded: true, userId: null as string | null, onboardingActive: false };
|
||||
|
||||
test('the veteran this exists for: settings loaded, no account, no onboarding', () => {
|
||||
assert.equal(shouldRequireSignIn(base), true);
|
||||
});
|
||||
|
||||
test('a signed-in user is never walled', () => {
|
||||
assert.equal(shouldRequireSignIn({ ...base, userId: 'u-1' }), false);
|
||||
});
|
||||
|
||||
test('nothing shows until settings are read, so a launch does not flash a login wall', () => {
|
||||
assert.equal(shouldRequireSignIn({ ...base, settingsLoaded: false }), false);
|
||||
});
|
||||
|
||||
test('a backend that never answers leaves the app usable rather than bricked', () => {
|
||||
// settingsLoaded stays false forever in that case; the wall must stay down, not go up.
|
||||
assert.equal(shouldRequireSignIn({ settingsLoaded: false, userId: null, onboardingActive: false }), false);
|
||||
});
|
||||
|
||||
test('onboarding owns the screen, so the gate stands down while its own sign-in beat runs', () => {
|
||||
assert.equal(shouldRequireSignIn({ ...base, onboardingActive: true }), false);
|
||||
});
|
||||
|
||||
test('a fresh install that finishes onboarding signed in stays down afterwards', () => {
|
||||
assert.equal(shouldRequireSignIn({ settingsLoaded: true, userId: 'u-2', onboardingActive: false }), false);
|
||||
});
|
||||
|
||||
test('an empty-string user id is not an account', () => {
|
||||
assert.equal(shouldRequireSignIn({ ...base, userId: '' }), true);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// Whether the mandatory sign-in wall should be up right now.
|
||||
//
|
||||
// Split out of the component so the branch that can lock every user out of the app is testable
|
||||
// without a React harness. See SignInRequiredGate.tsx for why the wall exists at all.
|
||||
export interface SignInGateState {
|
||||
settingsLoaded: boolean;
|
||||
userId: string | null;
|
||||
onboardingActive: boolean;
|
||||
}
|
||||
|
||||
export function shouldRequireSignIn({ settingsLoaded, userId, onboardingActive }: SignInGateState): boolean {
|
||||
// Fails OPEN until settings are read: a backend that never answers must not brick a local-first
|
||||
// app behind a wall the user's own saved account would have taken down.
|
||||
if (!settingsLoaded) return false;
|
||||
// Onboarding carries its own sign-in beat and its own curtain; stacking a second one hides both.
|
||||
if (onboardingActive) return false;
|
||||
return !userId;
|
||||
}
|
||||
Reference in New Issue
Block a user