('@/api/auth')
+ return {
+ ...actual,
+ sendVerifyCode: (...args: any[]) => sendVerifyCode(...args)
+ }
+})
+
+describe('PendingOAuthCreateAccountForm', () => {
+ beforeEach(() => {
+ sendVerifyCode.mockReset()
+ })
+
+ it('emits trimmed email, password, and verify code on submit', async () => {
+ const wrapper = mount(PendingOAuthCreateAccountForm, {
+ props: {
+ providerName: 'LinuxDo',
+ testIdPrefix: 'linuxdo',
+ initialEmail: 'prefill@example.com',
+ isSubmitting: false
+ }
+ })
+
+ await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' user@example.com ')
+ await wrapper.get('[data-testid="linuxdo-create-account-password"]').setValue('secret-123')
+ await wrapper.get('[data-testid="linuxdo-create-account-verify-code"]').setValue(' 246810 ')
+ await wrapper.get('form').trigger('submit.prevent')
+
+ expect(wrapper.emitted('submit')).toEqual([
+ [
+ {
+ email: 'user@example.com',
+ password: 'secret-123',
+ verifyCode: '246810'
+ }
+ ]
+ ])
+ })
+
+ it('sends a verify code for the trimmed email value', async () => {
+ sendVerifyCode.mockResolvedValue({
+ message: 'sent',
+ countdown: 60
+ })
+
+ const wrapper = mount(PendingOAuthCreateAccountForm, {
+ props: {
+ providerName: 'LinuxDo',
+ testIdPrefix: 'linuxdo',
+ initialEmail: '',
+ isSubmitting: false
+ }
+ })
+
+ await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' user@example.com ')
+ await wrapper.get('[data-testid="linuxdo-create-account-send-code"]').trigger('click')
+ await flushPromises()
+
+ expect(sendVerifyCode).toHaveBeenCalledWith({
+ email: 'user@example.com'
+ })
+ })
+})
diff --git a/frontend/src/views/auth/LinuxDoCallbackView.vue b/frontend/src/views/auth/LinuxDoCallbackView.vue
index ce7f92ef..128410bc 100644
--- a/frontend/src/views/auth/LinuxDoCallbackView.vue
+++ b/frontend/src/views/auth/LinuxDoCallbackView.vue
@@ -113,37 +113,14 @@
Enter an email address to create your account and continue.
-
-
-
-
-
-
-
- {{ accountActionError }}
-
-
+
@@ -258,6 +235,9 @@ import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
+import PendingOAuthCreateAccountForm, {
+ type PendingOAuthCreateAccountPayload
+} from '@/components/auth/PendingOAuthCreateAccountForm.vue'
import Icon from '@/components/icons/Icon.vue'
import { apiClient } from '@/api/client'
import { useAuthStore, useAppStore } from '@/stores'
@@ -432,9 +412,9 @@ function applyTotpChallenge(completion: LinuxDoPendingActionResponse): boolean {
return true
}
-function switchToBindLoginMode() {
+function switchToBindLoginMode(nextEmail?: string) {
pendingAccountAction.value = 'bind_login'
- bindLoginEmail.value = bindLoginEmail.value.trim() || pendingAccountEmail.value.trim()
+ bindLoginEmail.value = bindLoginEmail.value.trim() || nextEmail?.trim() || pendingAccountEmail.value.trim()
bindLoginPassword.value = ''
accountActionError.value = ''
canReturnToCreateAccount.value = true
@@ -533,15 +513,16 @@ async function handleContinueLogin() {
}
}
-async function handleCreateAccount() {
+async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
accountActionError.value = ''
- const email = pendingAccountEmail.value.trim()
- if (!email) return
+ if (!payload.email || !payload.password) return
isSubmitting.value = true
try {
const { data } = await apiClient.post('/auth/oauth/pending/create-account', {
- email,
+ email: payload.email,
+ password: payload.password,
+ verify_code: payload.verifyCode || undefined,
...serializeAdoptionDecision(currentAdoptionDecision())
})
await finalizePendingAccountResponse(data)
diff --git a/frontend/src/views/auth/OidcCallbackView.vue b/frontend/src/views/auth/OidcCallbackView.vue
index de3f2e40..344c3537 100644
--- a/frontend/src/views/auth/OidcCallbackView.vue
+++ b/frontend/src/views/auth/OidcCallbackView.vue
@@ -122,37 +122,14 @@
Enter an email address to create your account and continue.
-
-
-
-
-
-
-
- {{ accountActionError }}
-
-
+
@@ -267,6 +244,9 @@ import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
+import PendingOAuthCreateAccountForm, {
+ type PendingOAuthCreateAccountPayload
+} from '@/components/auth/PendingOAuthCreateAccountForm.vue'
import Icon from '@/components/icons/Icon.vue'
import { apiClient } from '@/api/client'
import { useAuthStore, useAppStore } from '@/stores'
@@ -476,9 +456,9 @@ function applyTotpChallenge(completion: PendingOidcCompletion): boolean {
return true
}
-function switchToBindLoginMode() {
+function switchToBindLoginMode(nextEmail?: string) {
pendingAccountAction.value = 'bind_login'
- bindLoginEmail.value = bindLoginEmail.value.trim() || pendingAccountEmail.value.trim()
+ bindLoginEmail.value = bindLoginEmail.value.trim() || nextEmail?.trim() || pendingAccountEmail.value.trim()
bindLoginPassword.value = ''
accountActionError.value = ''
canReturnToCreateAccount.value = true
@@ -577,15 +557,16 @@ async function handleContinueLogin() {
}
}
-async function handleCreateAccount() {
+async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
accountActionError.value = ''
- const email = pendingAccountEmail.value.trim()
- if (!email) return
+ if (!payload.email || !payload.password) return
isSubmitting.value = true
try {
const { data } = await apiClient.post('/auth/oauth/pending/create-account', {
- email,
+ email: payload.email,
+ password: payload.password,
+ verify_code: payload.verifyCode || undefined,
...serializeAdoptionDecision(currentAdoptionDecision())
})
await finalizePendingAccountResponse(data)
diff --git a/frontend/src/views/auth/WechatCallbackView.vue b/frontend/src/views/auth/WechatCallbackView.vue
index e4dd6301..10b83b1c 100644
--- a/frontend/src/views/auth/WechatCallbackView.vue
+++ b/frontend/src/views/auth/WechatCallbackView.vue
@@ -160,37 +160,14 @@
Enter an email address to create your account and continue.
-
-
-
-
-
-
-
- {{ accountActionError }}
-
-
+
@@ -305,6 +282,9 @@ import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
+import PendingOAuthCreateAccountForm, {
+ type PendingOAuthCreateAccountPayload
+} from '@/components/auth/PendingOAuthCreateAccountForm.vue'
import Icon from '@/components/icons/Icon.vue'
import { apiClient } from '@/api/client'
import { useAuthStore, useAppStore } from '@/stores'
@@ -575,9 +555,9 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
return true
}
-function switchToBindLoginMode() {
+function switchToBindLoginMode(nextEmail?: string) {
pendingAccountAction.value = 'bind_login'
- bindLoginEmail.value = bindLoginEmail.value.trim() || pendingAccountEmail.value.trim()
+ bindLoginEmail.value = bindLoginEmail.value.trim() || nextEmail?.trim() || pendingAccountEmail.value.trim()
bindLoginPassword.value = ''
accountActionError.value = ''
canReturnToCreateAccount.value = true
@@ -676,15 +656,16 @@ async function handleContinueLogin() {
}
}
-async function handleCreateAccount() {
+async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
accountActionError.value = ''
- const email = pendingAccountEmail.value.trim()
- if (!email) return
+ if (!payload.email || !payload.password) return
isSubmitting.value = true
try {
const { data } = await apiClient.post('/auth/oauth/pending/create-account', {
- email,
+ email: payload.email,
+ password: payload.password,
+ verify_code: payload.verifyCode || undefined,
...serializeAdoptionDecision(currentAdoptionDecision())
})
await finalizePendingAccountResponse(data)
diff --git a/frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts b/frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts
index 8b2482fa..b9930b70 100644
--- a/frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts
+++ b/frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts
@@ -11,6 +11,7 @@ const exchangePendingOAuthCompletion = vi.fn()
const completeLinuxDoOAuthRegistration = vi.fn()
const login2FA = vi.fn()
const apiClientPost = vi.fn()
+const sendVerifyCode = vi.fn()
vi.mock('vue-router', () => ({
useRoute: () => ({
@@ -53,7 +54,8 @@ vi.mock('@/api/auth', async () => {
...actual,
exchangePendingOAuthCompletion: (...args: any[]) => exchangePendingOAuthCompletion(...args),
completeLinuxDoOAuthRegistration: (...args: any[]) => completeLinuxDoOAuthRegistration(...args),
- login2FA: (...args: any[]) => login2FA(...args)
+ login2FA: (...args: any[]) => login2FA(...args),
+ sendVerifyCode: (...args: any[]) => sendVerifyCode(...args)
}
})
@@ -67,6 +69,7 @@ describe('LinuxDoCallbackView', () => {
completeLinuxDoOAuthRegistration.mockReset()
login2FA.mockReset()
apiClientPost.mockReset()
+ sendVerifyCode.mockReset()
})
it('does not send adoption decisions during the initial exchange', async () => {
@@ -251,7 +254,7 @@ describe('LinuxDoCallbackView', () => {
})
})
- it('collects email for pending oauth account creation and submits adoption decisions', async () => {
+ it('collects email, password, and verify code for pending oauth account creation and submits adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'email_required',
redirect: '/welcome',
@@ -286,11 +289,15 @@ describe('LinuxDoCallbackView', () => {
expect(checkboxes).toHaveLength(2)
await checkboxes[1].setValue(false)
await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' new@example.com ')
+ await wrapper.get('[data-testid="linuxdo-create-account-password"]').setValue('secret-123')
+ await wrapper.get('[data-testid="linuxdo-create-account-verify-code"]').setValue('246810')
await wrapper.get('[data-testid="linuxdo-create-account-submit"]').trigger('click')
await flushPromises()
expect(apiClientPost).toHaveBeenCalledWith('/auth/oauth/pending/create-account', {
email: 'new@example.com',
+ password: 'secret-123',
+ verify_code: '246810',
adopt_display_name: true,
adopt_avatar: false
})
@@ -298,6 +305,38 @@ describe('LinuxDoCallbackView', () => {
expect(replace).toHaveBeenCalledWith('/welcome')
})
+ it('sends a verify code for pending oauth account creation', async () => {
+ exchangePendingOAuthCompletion.mockResolvedValue({
+ error: 'email_required',
+ redirect: '/welcome'
+ })
+ sendVerifyCode.mockResolvedValue({
+ message: 'sent',
+ countdown: 60
+ })
+
+ const wrapper = mount(LinuxDoCallbackView, {
+ global: {
+ stubs: {
+ AuthLayout: { template: '
' },
+ Icon: true,
+ RouterLink: { template: '' },
+ transition: false
+ }
+ }
+ })
+
+ await flushPromises()
+
+ await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' new@example.com ')
+ await wrapper.get('[data-testid="linuxdo-create-account-send-code"]').trigger('click')
+ await flushPromises()
+
+ expect(sendVerifyCode).toHaveBeenCalledWith({
+ email: 'new@example.com'
+ })
+ })
+
it('shows bind-login form for existing account binding and submits credentials with adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'bind_login_required',
diff --git a/frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts b/frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts
index c7460d8f..cb28e283 100644
--- a/frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts
+++ b/frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts
@@ -12,6 +12,7 @@ const completeOIDCOAuthRegistration = vi.fn()
const getPublicSettings = vi.fn()
const login2FA = vi.fn()
const apiClientPost = vi.fn()
+const sendVerifyCode = vi.fn()
vi.mock('vue-router', () => ({
useRoute: () => ({
@@ -60,7 +61,8 @@ vi.mock('@/api/auth', async () => {
exchangePendingOAuthCompletion: (...args: any[]) => exchangePendingOAuthCompletion(...args),
completeOIDCOAuthRegistration: (...args: any[]) => completeOIDCOAuthRegistration(...args),
getPublicSettings: (...args: any[]) => getPublicSettings(...args),
- login2FA: (...args: any[]) => login2FA(...args)
+ login2FA: (...args: any[]) => login2FA(...args),
+ sendVerifyCode: (...args: any[]) => sendVerifyCode(...args)
}
})
@@ -75,6 +77,7 @@ describe('OidcCallbackView', () => {
getPublicSettings.mockReset()
login2FA.mockReset()
apiClientPost.mockReset()
+ sendVerifyCode.mockReset()
getPublicSettings.mockResolvedValue({
oidc_oauth_provider_name: 'ExampleID'
})
@@ -234,7 +237,7 @@ describe('OidcCallbackView', () => {
})
})
- it('collects email for pending oauth account creation and submits adoption decisions', async () => {
+ it('collects email, password, and verify code for pending oauth account creation and submits adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'email_required',
redirect: '/welcome',
@@ -269,11 +272,15 @@ describe('OidcCallbackView', () => {
expect(checkboxes).toHaveLength(2)
await checkboxes[1].setValue(false)
await wrapper.get('[data-testid="oidc-create-account-email"]').setValue(' new@example.com ')
+ await wrapper.get('[data-testid="oidc-create-account-password"]').setValue('secret-123')
+ await wrapper.get('[data-testid="oidc-create-account-verify-code"]').setValue('246810')
await wrapper.get('[data-testid="oidc-create-account-submit"]').trigger('click')
await flushPromises()
expect(apiClientPost).toHaveBeenCalledWith('/auth/oauth/pending/create-account', {
email: 'new@example.com',
+ password: 'secret-123',
+ verify_code: '246810',
adopt_display_name: true,
adopt_avatar: false
})
@@ -281,6 +288,38 @@ describe('OidcCallbackView', () => {
expect(replace).toHaveBeenCalledWith('/welcome')
})
+ it('sends a verify code for pending oauth account creation', async () => {
+ exchangePendingOAuthCompletion.mockResolvedValue({
+ error: 'email_required',
+ redirect: '/welcome'
+ })
+ sendVerifyCode.mockResolvedValue({
+ message: 'sent',
+ countdown: 60
+ })
+
+ const wrapper = mount(OidcCallbackView, {
+ global: {
+ stubs: {
+ AuthLayout: { template: '
' },
+ Icon: true,
+ RouterLink: { template: '' },
+ transition: false
+ }
+ }
+ })
+
+ await flushPromises()
+
+ await wrapper.get('[data-testid="oidc-create-account-email"]').setValue(' new@example.com ')
+ await wrapper.get('[data-testid="oidc-create-account-send-code"]').trigger('click')
+ await flushPromises()
+
+ expect(sendVerifyCode).toHaveBeenCalledWith({
+ email: 'new@example.com'
+ })
+ })
+
it('shows bind-login form for existing account binding and submits credentials with adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'adopt_existing_user_by_email',
diff --git a/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts b/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
index c49d0243..aa673238 100644
--- a/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
+++ b/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
@@ -7,6 +7,7 @@ const {
completeWeChatOAuthRegistrationMock,
login2FAMock,
apiClientPostMock,
+ sendVerifyCodeMock,
prepareOAuthBindAccessTokenCookieMock,
getAuthTokenMock,
replaceMock,
@@ -20,6 +21,7 @@ const {
completeWeChatOAuthRegistrationMock: vi.fn(),
login2FAMock: vi.fn(),
apiClientPostMock: vi.fn(),
+ sendVerifyCodeMock: vi.fn(),
prepareOAuthBindAccessTokenCookieMock: vi.fn(),
getAuthTokenMock: vi.fn(),
replaceMock: vi.fn(),
@@ -118,6 +120,7 @@ vi.mock('@/api/auth', async () => {
exchangePendingOAuthCompletion: (...args: any[]) => exchangePendingOAuthCompletionMock(...args),
completeWeChatOAuthRegistration: (...args: any[]) => completeWeChatOAuthRegistrationMock(...args),
login2FA: (...args: any[]) => login2FAMock(...args),
+ sendVerifyCode: (...args: any[]) => sendVerifyCodeMock(...args),
prepareOAuthBindAccessTokenCookie: (...args: any[]) => prepareOAuthBindAccessTokenCookieMock(...args),
getAuthToken: (...args: any[]) => getAuthTokenMock(...args),
}
@@ -129,6 +132,7 @@ describe('WechatCallbackView', () => {
completeWeChatOAuthRegistrationMock.mockReset()
login2FAMock.mockReset()
apiClientPostMock.mockReset()
+ sendVerifyCodeMock.mockReset()
replaceMock.mockReset()
setTokenMock.mockReset()
showSuccessMock.mockReset()
@@ -374,7 +378,7 @@ describe('WechatCallbackView', () => {
expect(locationState.current.href).toContain('mode=open')
})
- it('collects email for pending oauth account creation and submits adoption decisions', async () => {
+ it('collects email, password, and verify code for pending oauth account creation and submits adoption decisions', async () => {
exchangePendingOAuthCompletionMock.mockResolvedValue({
error: 'email_required',
redirect: '/welcome',
@@ -409,11 +413,15 @@ describe('WechatCallbackView', () => {
expect(checkboxes).toHaveLength(2)
await checkboxes[1].setValue(false)
await wrapper.get('[data-testid="wechat-create-account-email"]').setValue(' new@example.com ')
+ await wrapper.get('[data-testid="wechat-create-account-password"]').setValue('secret-123')
+ await wrapper.get('[data-testid="wechat-create-account-verify-code"]').setValue('246810')
await wrapper.get('[data-testid="wechat-create-account-submit"]').trigger('click')
await flushPromises()
expect(apiClientPostMock).toHaveBeenCalledWith('/auth/oauth/pending/create-account', {
email: 'new@example.com',
+ password: 'secret-123',
+ verify_code: '246810',
adopt_display_name: true,
adopt_avatar: false,
})
@@ -421,6 +429,38 @@ describe('WechatCallbackView', () => {
expect(replaceMock).toHaveBeenCalledWith('/welcome')
})
+ it('sends a verify code for pending oauth account creation', async () => {
+ exchangePendingOAuthCompletionMock.mockResolvedValue({
+ error: 'email_required',
+ redirect: '/welcome',
+ })
+ sendVerifyCodeMock.mockResolvedValue({
+ message: 'sent',
+ countdown: 60,
+ })
+
+ const wrapper = mount(WechatCallbackView, {
+ global: {
+ stubs: {
+ AuthLayout: { template: '
' },
+ Icon: true,
+ RouterLink: { template: '' },
+ transition: false,
+ },
+ },
+ })
+
+ await flushPromises()
+
+ await wrapper.get('[data-testid="wechat-create-account-email"]').setValue(' new@example.com ')
+ await wrapper.get('[data-testid="wechat-create-account-send-code"]').trigger('click')
+ await flushPromises()
+
+ expect(sendVerifyCodeMock).toHaveBeenCalledWith({
+ email: 'new@example.com',
+ })
+ })
+
it('shows bind-login form for existing account binding and submits credentials with adoption decisions', async () => {
exchangePendingOAuthCompletionMock.mockResolvedValue({
step: 'bind_login_required',