mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-06 12:53:01 +08:00
fix(auth-ui): gate pending OAuth account creation on a captcha proof
#5261 added a captcha check to POST /auth/oauth/pending/create-account, but the shared create-account form only gates its send-code button on the Turnstile token — the submit button's disabled condition never included it. Turnstile tokens are single-use, so handleSendCode resets the widget in its finally block and clears the token. Submitting inside the window before the widget re-solves omits turnstile_token from the payload, and the backend answers ErrTurnstileVerificationFailed (turnstile_service.go:65). With an interaction-required challenge the widget does not re-solve on its own, so the failure persists until the user notices and verifies again — while the button stays enabled and says nothing. Gate the submit button on the token, mirroring the send-code button and EmailVerifyView, which already gates its pending-OAuth submit the same way. handleSubmit repeats the check because implicit form submission (Enter in a text input) bypasses the button's disabled state. The Tencent path is unaffected: handleSubmit already acquires a fresh proof per submit, and turnstile_enabled is false in that configuration. Verified with the component spec (11 passed) and vue-tsc --noEmit (clean).
This commit is contained in:
@@ -75,7 +75,7 @@
|
||||
:data-testid="`${testIdPrefix}-create-account-submit`"
|
||||
type="button"
|
||||
class="btn btn-primary w-full"
|
||||
:disabled="isSubmitting || !email.trim() || password.length < 6 || (invitationCodeEnabled && !invitationCode.trim())"
|
||||
:disabled="isSubmitting || !email.trim() || password.length < 6 || (invitationCodeEnabled && !invitationCode.trim()) || (turnstileEnabled && !turnstileToken)"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isSubmitting ? t('common.processing') : t('auth.createAccount') }}
|
||||
@@ -284,6 +284,14 @@ async function handleSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
// Turnstile 票据一次性:发送验证码已消耗上一枚,reset 后要等新票据回调。
|
||||
// 缺票时不能提交——create-account 端点会校验验证码,空 token 直接被判失败。
|
||||
// 表单的隐式提交(输入框回车)绕得过按钮的 disabled,所以这里必须再挡一次。
|
||||
if (turnstileEnabled.value && !turnstileToken.value) {
|
||||
sendCodeError.value = t('auth.completeVerification')
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireTencentProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -328,4 +328,99 @@ describe('PendingOAuthCreateAccountForm', () => {
|
||||
turnstile_token: 'turnstile-token'
|
||||
})
|
||||
})
|
||||
|
||||
it('requires a turnstile token before submitting when turnstile is enabled', async () => {
|
||||
getPublicSettings.mockResolvedValue({
|
||||
turnstile_enabled: true,
|
||||
turnstile_site_key: 'site-key'
|
||||
})
|
||||
|
||||
const wrapper = mount(PendingOAuthCreateAccountForm, {
|
||||
props: {
|
||||
testIdPrefix: 'linuxdo',
|
||||
initialEmail: 'user@example.com',
|
||||
isSubmitting: false
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
TurnstileWidget: {
|
||||
template: '<button data-testid="turnstile-verify" @click="$emit(\'verify\', \'turnstile-token\')">verify</button>',
|
||||
methods: { reset: turnstileReset }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await wrapper.get('[data-testid="linuxdo-create-account-password"]').setValue('secret-123')
|
||||
|
||||
expect(wrapper.get('[data-testid="linuxdo-create-account-submit"]').attributes('disabled')).toBeDefined()
|
||||
|
||||
// 隐式提交(输入框回车)绕过按钮 disabled,仍不能带着空票据发出请求
|
||||
await wrapper.get('form').trigger('submit.prevent')
|
||||
expect(wrapper.emitted('submit')).toBeUndefined()
|
||||
|
||||
await wrapper.get('[data-testid="turnstile-verify"]').trigger('click')
|
||||
|
||||
expect(wrapper.get('[data-testid="linuxdo-create-account-submit"]').attributes('disabled')).toBeUndefined()
|
||||
|
||||
await wrapper.get('[data-testid="linuxdo-create-account-submit"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('submit')).toEqual([
|
||||
[
|
||||
{
|
||||
email: 'user@example.com',
|
||||
password: 'secret-123',
|
||||
verifyCode: '',
|
||||
turnstileToken: 'turnstile-token',
|
||||
invitationCode: undefined
|
||||
}
|
||||
]
|
||||
])
|
||||
})
|
||||
|
||||
it('blocks submit again after sending a verify code consumes the turnstile proof', async () => {
|
||||
getPublicSettings.mockResolvedValue({
|
||||
turnstile_enabled: true,
|
||||
turnstile_site_key: 'site-key'
|
||||
})
|
||||
sendPendingOAuthVerifyCode.mockResolvedValue({ message: 'sent', countdown: 60 })
|
||||
|
||||
const wrapper = mount(PendingOAuthCreateAccountForm, {
|
||||
props: {
|
||||
testIdPrefix: 'linuxdo',
|
||||
initialEmail: 'user@example.com',
|
||||
isSubmitting: false
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
TurnstileWidget: {
|
||||
template: '<button data-testid="turnstile-verify" @click="$emit(\'verify\', \'turnstile-token\')">verify</button>',
|
||||
methods: { reset: turnstileReset }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await wrapper.get('[data-testid="linuxdo-create-account-password"]').setValue('secret-123')
|
||||
await wrapper.get('[data-testid="turnstile-verify"]').trigger('click')
|
||||
await wrapper.get('[data-testid="linuxdo-create-account-send-code"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
// 发码消耗掉票据并 reset 组件,新票据回调前提交必须保持关闭
|
||||
expect(turnstileReset).toHaveBeenCalled()
|
||||
expect(wrapper.get('[data-testid="linuxdo-create-account-submit"]').attributes('disabled')).toBeDefined()
|
||||
|
||||
await wrapper.get('form').trigger('submit.prevent')
|
||||
expect(wrapper.emitted('submit')).toBeUndefined()
|
||||
|
||||
// 新票据回调后恢复可提交,且带上新票据
|
||||
await wrapper.get('[data-testid="turnstile-verify"]').trigger('click')
|
||||
await wrapper.get('[data-testid="linuxdo-create-account-submit"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('submit')?.[0]?.[0]).toMatchObject({
|
||||
turnstileToken: 'turnstile-token'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user