Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions frontend/src/entities/user/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ describe('createUserApis', () => {
await apis.login({
email: 'reader@example.com',
password: 'password-123',
code: '123456',
})
await apis.loginByCode({ email: 'reader@example.com', code: '123456' })
await apis.refresh('refresh-token')
Expand Down Expand Up @@ -89,7 +88,6 @@ describe('createUserApis', () => {
json: {
email: 'reader@example.com',
password: 'password-123',
code: '123456',
},
},
],
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/entities/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export interface UserApis {
code: string
nickname?: string
}): Promise<AuthTokens>
login(input: { email: string; password: string; code: string }): Promise<AuthTokens>
/** 密码登录不带验证码;验证码只用于注册、免密登录与重设密码。 */
login(input: { email: string; password: string }): Promise<AuthTokens>
loginByCode(input: { email: string; code: string }): Promise<AuthTokens>
refresh(refreshToken: string): Promise<AuthTokens>
logout(refreshToken: string): Promise<void>
Expand Down
15 changes: 5 additions & 10 deletions frontend/src/features/account-panel/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,31 +207,26 @@ describe('AccountPanel', () => {
expect(screen.getByTestId('location').textContent).toBe('/')
})

it('submits password login with a login-purpose code and keeps recovery visibly unavailable', async () => {
it('submits password login without an email code and keeps recovery visibly unavailable', async () => {
const { apis } = renderPanel()
fireEvent.click(screen.getByRole('tab', { name: '密码登录' }))
expect(screen.getByText('忘记密码')).toBeTruthy()
expect(screen.getByText('暂未开放')).toBeTruthy()
expect(screen.queryByLabelText('验证码')).toBeNull()
expect(screen.queryByRole('button', { name: '发送验证码' })).toBeNull()
expect(screen.getByText('用邮箱和密码直接登录。')).toBeTruthy()

fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'reader@example.com' } })
fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'password-123' } })
fireEvent.change(screen.getByLabelText('验证码'), { target: { value: '123456' } })
fireEvent.click(screen.getByRole('button', { name: '发送验证码' }))
await waitFor(() =>
expect(apis.sendCode).toHaveBeenCalledWith({
email: 'reader@example.com',
purpose: 'login',
}),
)

fireEvent.submit(screen.getByRole('button', { name: '登录' }).closest('form')!)
await waitFor(() =>
expect(apis.login).toHaveBeenCalledWith({
email: 'reader@example.com',
password: 'password-123',
code: '123456',
}),
)
expect(apis.sendCode).not.toHaveBeenCalled()
})

it('validates registration fields and sends register-purpose codes', async () => {
Expand Down
66 changes: 34 additions & 32 deletions frontend/src/features/account-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const modeCopy: Record<
password: {
tab: '密码登录',
title: '登录 Windup',
description: '使用密码和邮箱验证码确认身份。',
description: '用邮箱和密码直接登录。',
submit: '登录',
},
register: {
Expand Down Expand Up @@ -177,7 +177,7 @@ function AccountPanelDialog() {
if (mode !== 'code' && (password.length < 8 || password.length > 128)) {
return '密码需为 8–128 位'
}
if (!CODE_PATTERN.test(code)) return '验证码需为 6 位数字'
if (mode !== 'password' && !CODE_PATTERN.test(code)) return '验证码需为 6 位数字'
if (mode === 'register' && nickname.length > 50) return '昵称不能超过 50 个字符'
return null
}
Expand All @@ -200,7 +200,7 @@ function AccountPanelDialog() {
await session.loginByCode({ email: normalizedEmail, code })
successMessage = '登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。'
} else if (mode === 'password') {
await session.login({ email: normalizedEmail, password, code })
await session.login({ email: normalizedEmail, password })
successMessage = '登录成功,正在继续。'
} else {
await session.register({
Expand Down Expand Up @@ -379,35 +379,37 @@ function AccountPanelDialog() {
</div>
)}

<div className="grid gap-1.5 text-sm font-semibold text-[#344039]">
<label htmlFor={codeId}>验证码</label>
<span className="flex items-stretch gap-2">
<input
id={codeId}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={6}
value={code}
onChange={(event) => setCode(event.target.value)}
disabled={isSubmitting}
className={fieldClass}
placeholder="6 位数字"
/>
<button
type="button"
onClick={() => void sendCode()}
disabled={isSendingCode || isSubmitting || cooldownSeconds > 0}
className="min-h-11 min-w-[7.25rem] rounded-xl border border-[#607067] bg-[#eef2ef] px-3 text-sm font-semibold text-[#34483a] transition-colors hover:bg-[#e1e8e3] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#284331] disabled:cursor-not-allowed disabled:border-[#bbc2bc] disabled:text-[#858d87]"
>
{isSendingCode
? '正在发送…'
: cooldownSeconds > 0
? `${cooldownSeconds} 秒后重发`
: '发送验证码'}
</button>
</span>
</div>
{mode !== 'password' && (
<div className="grid gap-1.5 text-sm font-semibold text-[#344039]">
<label htmlFor={codeId}>验证码</label>
<span className="flex items-stretch gap-2">
<input
id={codeId}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={6}
value={code}
onChange={(event) => setCode(event.target.value)}
disabled={isSubmitting}
className={fieldClass}
placeholder="6 位数字"
/>
<button
type="button"
onClick={() => void sendCode()}
disabled={isSendingCode || isSubmitting || cooldownSeconds > 0}
className="min-h-11 min-w-[7.25rem] rounded-xl border border-[#607067] bg-[#eef2ef] px-3 text-sm font-semibold text-[#34483a] transition-colors hover:bg-[#e1e8e3] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#284331] disabled:cursor-not-allowed disabled:border-[#bbc2bc] disabled:text-[#858d87]"
>
{isSendingCode
? '正在发送…'
: cooldownSeconds > 0
? `${cooldownSeconds} 秒后重发`
: '发送验证码'}
</button>
</span>
</div>
)}

{mode === 'code' && (
<p className="rounded-xl border border-[#97a99b]/45 bg-[#edf3ee] px-3.5 py-3 text-sm leading-6 text-[#46564b]">
Expand Down
1 change: 0 additions & 1 deletion frontend/src/features/auth-session/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ describe('AuthSessionProvider', () => {
await session().login({
email: 'reader@example.com',
password: 'password-123',
code: '123456',
})
} else {
await session().loginByCode({ email: 'reader@example.com', code: '123456' })
Expand Down