From f52dc6fa9fafdf5611251ca68cf0dbce8c1879fc Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:53:42 +0800 Subject: [PATCH] feat(console): dev-seeded admin credentials hint on the login page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The framework seeds admin@objectos.ai/admin123 on an empty dev DB, but the login page gave no signal it exists — new users signed up and fell into an empty non-admin workspace (framework 15.1 third-party eval P2). Render a dismissible amber banner above the login card when /api/v1/auth/config reports devSeedAdmin. The field is server-gated: present only under NODE_ENV=development while the seed account still verifies against the default password (framework#3108), so production can never show it. Dismissal persists per browser (os.console.devAdminHintDismissed). Browser-verified against a real dev backend: banner renders (zh locale), dismiss hides + survives reload, clearing the flag restores it; absent config renders nothing. 3 new vitest cases. Co-Authored-By: Claude Fable 5 --- .changeset/login-dev-admin-hint.md | 6 ++ apps/console/src/pages/auth/LoginPage.tsx | 81 +++++++++++++++++- .../LoginPage.dev-admin-hint.test.tsx | 82 +++++++++++++++++++ packages/i18n/src/locales/en.ts | 5 ++ packages/i18n/src/locales/zh.ts | 5 ++ 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 .changeset/login-dev-admin-hint.md create mode 100644 apps/console/src/pages/auth/__tests__/LoginPage.dev-admin-hint.test.tsx diff --git a/.changeset/login-dev-admin-hint.md b/.changeset/login-dev-admin-hint.md new file mode 100644 index 000000000..e9ed48c0d --- /dev/null +++ b/.changeset/login-dev-admin-hint.md @@ -0,0 +1,6 @@ +--- +"@object-ui/console": patch +"@object-ui/i18n": patch +--- + +Login page surfaces the dev-seeded admin credentials. The framework runtime seeds `admin@objectos.ai` on an empty development database, but nothing on the login page said so — new users clicked "Sign up" and landed in an empty non-admin workspace (15.1 third-party eval). When `GET /api/v1/auth/config` reports `devSeedAdmin` (dev-only; the server omits the field in production and once the default password is changed), the page renders a dismissible amber banner with the credentials. Dismissal persists per browser via localStorage. diff --git a/apps/console/src/pages/auth/LoginPage.tsx b/apps/console/src/pages/auth/LoginPage.tsx index 2b2b46a16..09d2f91c5 100644 --- a/apps/console/src/pages/auth/LoginPage.tsx +++ b/apps/console/src/pages/auth/LoginPage.tsx @@ -44,6 +44,8 @@ function withConsoleBase(path: string): string { return base + (path.startsWith('/') ? path : `/${path}`); } +const DEV_HINT_DISMISSED_KEY = 'os.console.devAdminHintDismissed'; + function RouterLink(props: { href: string; className?: string; children: React.ReactNode }) { return ( @@ -67,6 +69,30 @@ export function LoginPage() { } = useAuth(); const [signUpDisabled, setSignUpDisabled] = useState(false); + // Dev-only seeded-admin hint (15.1 third-party eval): the runtime seeds + // admin@objectos.ai on an empty dev DB, but nothing on this page said so — + // new users clicked "Sign up" and landed in an empty non-admin workspace. + // The server reports the credentials via /auth/config `devSeedAdmin` ONLY + // in development while the account still carries the default password, so + // production can never render this. Dismissal is remembered per browser. + const [devSeedAdmin, setDevSeedAdmin] = useState<{ email: string; password?: string } | null>( + null, + ); + const [devHintDismissed, setDevHintDismissed] = useState(() => { + try { + return window.localStorage.getItem(DEV_HINT_DISMISSED_KEY) === '1'; + } catch { + return false; + } + }); + const dismissDevHint = () => { + setDevHintDismissed(true); + try { + window.localStorage.setItem(DEV_HINT_DISMISSED_KEY, '1'); + } catch { + /* private mode — hide for this session only */ + } + }; const [autoSelectingOrg, setAutoSelectingOrg] = useState(false); // The OAuth hand-off fetch must fire at most once even though the post-login // effect re-runs as org state settles (it navigates away on success). @@ -111,16 +137,27 @@ export function LoginPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Read public auth config once to know whether sign-up is gated off. + // Read public auth config once to know whether sign-up is gated off and + // whether the dev-seeded admin credentials should be surfaced. useEffect(() => { let cancelled = false; getAuthConfig() .then((cfg) => { if (cancelled) return; setSignUpDisabled(cfg?.emailPassword?.disableSignUp === true); + const seed = (cfg as { devSeedAdmin?: { email?: unknown; password?: unknown } } | null) + ?.devSeedAdmin; + setDevSeedAdmin( + seed && typeof seed.email === 'string' + ? { + email: seed.email, + password: typeof seed.password === 'string' ? seed.password : undefined, + } + : null, + ); }) .catch(() => { - /* leave default (false) — server-side gate is the source of truth */ + /* leave defaults — server-side gate is the source of truth */ }); return () => { cancelled = true; @@ -227,6 +264,46 @@ export function LoginPage() { ) : null} + {devSeedAdmin && !devHintDismissed ? ( +
+ +
+
+ {t('auth.login.devAdminHint.title', { defaultValue: 'Development instance' })} +
+
+ {t('auth.login.devAdminHint.body', { + defaultValue: 'Sign in with the seeded dev admin:', + })}{' '} + + {devSeedAdmin.email} + + {devSeedAdmin.password ? ( + <> + {' / '} + + {devSeedAdmin.password} + + + ) : null} +
+
+ +
+ ) : null} { + window.localStorage.clear(); +}); + +function createMockClient(config: Record): AuthClient { + return { + getSession: vi.fn().mockResolvedValue(null), + getConfig: vi.fn().mockResolvedValue(config), + } as unknown as AuthClient; +} + +function renderLogin(config: Record) { + window.history.replaceState({}, '', '/login'); + return render( + + + + + , + ); +} + +const DEV_SEED = { devSeedAdmin: { email: 'admin@objectos.ai', password: 'admin123' } }; + +describe('LoginPage — dev-seeded admin hint', () => { + it('renders the credentials when the server reports devSeedAdmin', async () => { + renderLogin(DEV_SEED); + + const hint = await screen.findByTestId('dev-admin-hint'); + expect(hint.textContent).toContain('admin@objectos.ai'); + expect(hint.textContent).toContain('admin123'); + }); + + it('renders nothing without devSeedAdmin (production config)', async () => { + renderLogin({ emailPassword: { disableSignUp: false } }); + + // Let the config effect settle, then assert absence. + await waitFor(() => expect(screen.queryByTestId('dev-admin-hint')).toBeNull()); + }); + + it('dismisses on click and stays dismissed via localStorage', async () => { + renderLogin(DEV_SEED); + const user = userEvent.setup(); + + await screen.findByTestId('dev-admin-hint'); + await user.click(screen.getByRole('button', { name: 'Dismiss' })); + expect(screen.queryByTestId('dev-admin-hint')).toBeNull(); + expect(window.localStorage.getItem('os.console.devAdminHintDismissed')).toBe('1'); + + // A re-render (new visit) must respect the stored dismissal. + cleanup(); + renderLogin(DEV_SEED); + await waitFor(() => expect(screen.queryByTestId('dev-admin-hint')).toBeNull()); + }); +}); diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 345a9fadd..6ca7716a3 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -1632,6 +1632,11 @@ const en = { signUpText: 'Sign up', signingIn: 'Signing you in…', ssoHandoff: 'Continue to {{target}}', + devAdminHint: { + title: 'Development instance', + body: 'Sign in with the seeded dev admin:', + dismiss: 'Dismiss', + }, errors: { invalidCredentials: 'Invalid email or password. Please try again.', emailNotVerified: 'Please verify your email address before signing in.', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index f5644ac53..c29f0188c 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -1705,6 +1705,11 @@ const zh = { signUpText: '注册', signingIn: '正在登录…', ssoHandoff: '继续前往 {{target}}', + devAdminHint: { + title: '开发实例', + body: '可使用内置的开发管理员登录:', + dismiss: '关闭', + }, errors: { invalidCredentials: '邮箱或密码错误,请重试。', emailNotVerified: '请先验证您的邮箱后再登录。',