fix(auth): hide password routing implementation details

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-24 11:21:08 +08:00
parent 0e9d2e2d3e
commit 8249e84454
6 changed files with 20 additions and 17 deletions

View file

@ -8,9 +8,17 @@ test.describe('Auth Entry', () => {
})
test('validates required fields and preserves returnTo on register link', async ({ page }) => {
await page.route('**/runtime-config.js', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/javascript',
body: 'window.__SKILLHUB_RUNTIME_CONFIG__ = { authDirectEnabled: "true", authDirectProvider: "local" }',
})
})
await page.goto('/login?returnTo=%2Fdashboard%2Ftokens')
await expect(page.getByRole('heading', { name: 'Login to SkillHub' })).toBeVisible()
await expect(page.getByText(/password compatibility layer/i)).toHaveCount(0)
await page.getByRole('button', { name: 'Login' }).click()
await expect(page.getByText('Username is required')).toBeVisible()

View file

@ -360,7 +360,6 @@
"noAccount": "Don't have an account?",
"register": "Sign up now",
"oauthHint": "After OAuth authentication, you will be automatically redirected back to this site.",
"passwordCompatHint": "This deployment has the password compatibility layer enabled. The form will route to {{name}} instead of the fixed local account endpoint.",
"enterpriseSsoTitle": "Enterprise SSO",
"enterpriseSsoHint": "This deployment has the compatibility layer enabled. If your browser already has a {{name}} session, you can try establishing a SkillHub session directly.",
"enterpriseSsoAutoHint": "This deployment has automatic {{name}} probing enabled. If it does not succeed, you can continue with the standard login methods.",

View file

@ -360,7 +360,6 @@
"noAccount": "Нет аккаунта?",
"register": "Зарегистрироваться",
"oauthHint": "После аутентификации OAuth вы будете автоматически перенаправлены обратно на этот сайт.",
"passwordCompatHint": "В этом развёртывании включён слой совместимости паролей. Форма направит запрос в {{name}} вместо фиксированной локальной учётной записи.",
"enterpriseSsoTitle": "Корпоративный SSO",
"enterpriseSsoHint": "В этом развёртывании включён слой совместимости. Если в браузере уже есть сессия {{name}}, можно попробовать сразу установить сессию SkillHub.",
"enterpriseSsoAutoHint": "В этом развёртывании включено автоматическое зондирование {{name}}. Если оно не сработает, продолжайте стандартными способами входа.",

View file

@ -360,7 +360,6 @@
"noAccount": "还没有账号?",
"register": "立即注册",
"oauthHint": "使用 OAuth 登录时,认证完成后会自动返回当前站点。",
"passwordCompatHint": "当前部署已启用账号密码兼容接入层。表单将路由到 {{name}},而不是固定使用本地账号接口。",
"enterpriseSsoTitle": "企业单点登录",
"enterpriseSsoHint": "当前部署已启用兼容接入层。若浏览器中已存在 {{name}} 会话,可直接尝试建立 SkillHub 登录态。",
"enterpriseSsoAutoHint": "当前部署已启用自动 {{name}} 探测。若未成功,你仍可继续使用现有登录方式。",

View file

@ -6,6 +6,7 @@ import { describe, expect, it, vi } from 'vitest'
const authMethodsFixture = vi.hoisted(() => ({
methods: [] as Array<{ id: string, methodType: string }>,
bootstrapEnabled: false,
directEnabled: false,
isError: false,
returnTo: '',
navigate: vi.fn(),
@ -38,7 +39,7 @@ vi.mock('lucide-react', () => ({
}))
vi.mock('@/api/client', () => ({
getDirectAuthRuntimeConfig: () => ({ enabled: false }),
getDirectAuthRuntimeConfig: () => ({ enabled: authMethodsFixture.directEnabled, provider: 'local' }),
getSessionBootstrapRuntimeConfig: () => ({ enabled: authMethodsFixture.bootstrapEnabled, provider: 'proxy' }),
}))
@ -83,6 +84,7 @@ describe('LoginPage', () => {
cleanup()
authMethodsFixture.methods = []
authMethodsFixture.bootstrapEnabled = false
authMethodsFixture.directEnabled = false
authMethodsFixture.isError = false
authMethodsFixture.returnTo = ''
authMethodsFixture.navigate.mockClear()
@ -103,6 +105,14 @@ describe('LoginPage', () => {
expect(html).toContain('login.register')
})
it('does not expose password routing details when direct login is configured', () => {
authMethodsFixture.directEnabled = true
const html = renderToStaticMarkup(<LoginPage />)
expect(html).toContain('login.submit')
expect(html).not.toContain('login.passwordCompatHint')
})
it('returns to the home page after direct login without an explicit destination', async () => {
render(<LoginPage />)
fireEvent.change(screen.getByLabelText('login.username'), { target: { value: 'user1' } })

View file

@ -2,7 +2,7 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ArrowRight, Eye, EyeOff, LockKeyhole, UserRound } from 'lucide-react'
import { getDirectAuthRuntimeConfig, getSessionBootstrapRuntimeConfig } from '@/api/client'
import { getSessionBootstrapRuntimeConfig } from '@/api/client'
import { AuthShell } from '@/features/auth/auth-shell'
import { AuthMethodButtonList } from '@/features/auth/login-button'
import { SessionBootstrapEntry } from '@/features/auth/session-bootstrap-entry'
@ -23,7 +23,6 @@ export function LoginPage() {
const navigate = useNavigate()
const search = useSearch({ from: '/login' })
const loginMutation = usePasswordLogin()
const directAuthConfig = getDirectAuthRuntimeConfig()
const bootstrapConfig = getSessionBootstrapRuntimeConfig()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
@ -34,10 +33,6 @@ export function LoginPage() {
const returnTo = resolveAuthReturnTo(search.returnTo)
const { data: authMethods, isLoading: authMethodsLoading } = useAuthMethods(returnTo)
const disabledMessage = search.reason === 'accountDisabled' ? t('apiError.auth.accountDisabled') : null
const directMethod = directAuthConfig.provider
? authMethods?.find((method) =>
method.methodType === 'DIRECT_PASSWORD' && method.provider === directAuthConfig.provider)
: undefined
const bootstrapMethod = authMethods?.find((method) => method.methodType === 'SESSION_BOOTSTRAP')
const hasOrganizationMethod = bootstrapConfig.enabled
const hasExternalMethods = authMethods?.some((method) => method.methodType === 'OAUTH_REDIRECT')
@ -97,13 +92,6 @@ export function LoginPage() {
<div hidden={loginMode !== 'personal'}>
<form className="space-y-4" onSubmit={handleSubmit}>
{directAuthConfig.enabled ? (
<p className="rounded-xl border border-blue-500/20 bg-blue-500/10 px-3 py-2 text-sm text-blue-700 dark:text-blue-300">
{t('login.passwordCompatHint', {
name: directMethod?.displayName ?? directAuthConfig.provider,
})}
</p>
) : null}
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="username">{t('login.username')}</label>
<div className="relative">