Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
0fa47f18
Commit
0fa47f18
authored
Apr 21, 2026
by
IanShaw027
Browse files
feat: complete pending oauth account creation UI
parent
7ef7fd19
Changes
8
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/auth/PendingOAuthCreateAccountForm.vue
0 → 100644
View file @
0fa47f18
<
template
>
<form
class=
"space-y-3"
@
submit.prevent=
"handleSubmit"
>
<input
v-model=
"email"
:data-testid=
"`$
{testIdPrefix}-create-account-email`"
type="email"
class="input w-full"
placeholder="you@example.com"
:disabled="isSubmitting || isSendingCode"
/>
<input
v-model=
"password"
:data-testid=
"`$
{testIdPrefix}-create-account-password`"
type="password"
class="input w-full"
placeholder="Password"
:disabled="isSubmitting"
/>
<div
class=
"flex gap-3"
>
<input
v-model=
"verifyCode"
:data-testid=
"`$
{testIdPrefix}-create-account-verify-code`"
type="text"
inputmode="numeric"
maxlength="6"
class="input min-w-0 flex-1"
placeholder="123456"
:disabled="isSubmitting"
/>
<button
:data-testid=
"`$
{testIdPrefix}-create-account-send-code`"
type="button"
class="btn btn-secondary shrink-0"
:disabled="isSubmitting || isSendingCode || countdown > 0 || !email.trim()"
@click="handleSendCode"
>
{{
isSendingCode
?
t
(
'
auth.sendingCode
'
)
:
countdown
>
0
?
t
(
'
auth.resendCountdown
'
,
{
countdown
}
)
:
t
(
'
auth.sendCode
'
)
}}
<
/button
>
<
/div
>
<
p
v
-
if
=
"
sendCodeSuccess
"
class
=
"
text-sm text-green-600 dark:text-green-400
"
>
{{
t
(
'
auth.codeSentSuccess
'
)
}}
<
/p
>
<
p
v
-
else
class
=
"
text-xs text-gray-500 dark:text-dark-400
"
>
{{
t
(
'
auth.verificationCodeHint
'
)
}}
<
/p
>
<
button
:
data
-
testid
=
"
`${testIdPrefix
}
-create-account-submit`
"
type
=
"
button
"
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
isSubmitting || !email.trim() || password.length < 6
"
@
click
=
"
handleSubmit
"
>
{{
isSubmitting
?
t
(
'
common.processing
'
)
:
'
Create account
'
}}
<
/button
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
emitSwitchToBind
"
>
I
already
have
an
account
<
/button
>
<
transition
name
=
"
fade
"
>
<
p
v
-
if
=
"
sendCodeError
"
class
=
"
text-sm text-red-600 dark:text-red-400
"
>
{{
sendCodeError
}}
<
/p
>
<
/transition
>
<
transition
name
=
"
fade
"
>
<
p
v
-
if
=
"
errorMessage
"
class
=
"
text-sm text-red-600 dark:text-red-400
"
>
{{
errorMessage
}}
<
/p
>
<
/transition
>
<
/form
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
onUnmounted
,
ref
,
watch
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
sendVerifyCode
}
from
'
@/api/auth
'
export
type
PendingOAuthCreateAccountPayload
=
{
email
:
string
password
:
string
verifyCode
:
string
}
const
props
=
defineProps
<
{
initialEmail
:
string
testIdPrefix
:
string
isSubmitting
:
boolean
errorMessage
?:
string
}
>
()
const
emit
=
defineEmits
<
{
submit
:
[
payload
:
PendingOAuthCreateAccountPayload
]
switchToBind
:
[
email
:
string
]
}
>
()
const
{
t
}
=
useI18n
()
const
email
=
ref
(
''
)
const
password
=
ref
(
''
)
const
verifyCode
=
ref
(
''
)
const
isSendingCode
=
ref
(
false
)
const
sendCodeError
=
ref
(
''
)
const
sendCodeSuccess
=
ref
(
false
)
const
countdown
=
ref
(
0
)
let
countdownTimer
:
ReturnType
<
typeof
setInterval
>
|
null
=
null
watch
(
()
=>
props
.
initialEmail
,
value
=>
{
email
.
value
=
value
||
''
}
,
{
immediate
:
true
}
)
function
clearCountdown
()
{
if
(
countdownTimer
)
{
clearInterval
(
countdownTimer
)
countdownTimer
=
null
}
}
function
startCountdown
(
seconds
:
number
)
{
clearCountdown
()
countdown
.
value
=
Math
.
max
(
0
,
seconds
)
if
(
countdown
.
value
<=
0
)
{
return
}
countdownTimer
=
setInterval
(()
=>
{
if
(
countdown
.
value
<=
1
)
{
countdown
.
value
=
0
clearCountdown
()
return
}
countdown
.
value
-=
1
}
,
1000
)
}
function
getRequestErrorMessage
(
error
:
unknown
,
fallback
:
string
):
string
{
const
err
=
error
as
{
message
?:
string
;
response
?:
{
data
?:
{
detail
?:
string
;
message
?:
string
}
}
}
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
}
async
function
handleSendCode
()
{
const
trimmedEmail
=
email
.
value
.
trim
()
if
(
!
trimmedEmail
)
{
return
}
isSendingCode
.
value
=
true
sendCodeError
.
value
=
''
sendCodeSuccess
.
value
=
false
try
{
const
response
=
await
sendVerifyCode
({
email
:
trimmedEmail
}
)
sendCodeSuccess
.
value
=
true
startCountdown
(
response
.
countdown
)
}
catch
(
error
:
unknown
)
{
sendCodeError
.
value
=
getRequestErrorMessage
(
error
,
t
(
'
auth.sendCodeFailed
'
))
}
finally
{
isSendingCode
.
value
=
false
}
}
function
handleSubmit
()
{
const
trimmedEmail
=
email
.
value
.
trim
()
if
(
!
trimmedEmail
||
password
.
value
.
length
<
6
)
{
return
}
emit
(
'
submit
'
,
{
email
:
trimmedEmail
,
password
:
password
.
value
,
verifyCode
:
verifyCode
.
value
.
trim
()
}
)
}
function
emitSwitchToBind
()
{
emit
(
'
switchToBind
'
,
email
.
value
.
trim
())
}
onUnmounted
(()
=>
{
clearCountdown
()
}
)
<
/script
>
<
style
scoped
>
.
fade
-
enter
-
active
,
.
fade
-
leave
-
active
{
transition
:
all
0.3
s
ease
;
}
.
fade
-
enter
-
from
,
.
fade
-
leave
-
to
{
opacity
:
0
;
transform
:
translateY
(
-
8
px
);
}
<
/style
>
frontend/src/components/auth/__tests__/PendingOAuthCreateAccountForm.spec.ts
0 → 100644
View file @
0fa47f18
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
{
flushPromises
,
mount
}
from
'
@vue/test-utils
'
import
PendingOAuthCreateAccountForm
from
'
../PendingOAuthCreateAccountForm.vue
'
const
sendVerifyCode
=
vi
.
fn
()
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
vue-i18n
'
)
>
(
'
vue-i18n
'
)
return
{
...
actual
,
useI18n
:
()
=>
({
t
:
(
key
:
string
)
=>
key
})
}
})
vi
.
mock
(
'
@/api/auth
'
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
@/api/auth
'
)
>
(
'
@/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
'
})
})
})
frontend/src/views/auth/LinuxDoCallbackView.vue
View file @
0fa47f18
...
@@ -113,37 +113,14 @@
...
@@ -113,37 +113,14 @@
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
Enter an email address to create your account and continue.
Enter an email address to create your account and continue.
</p>
</p>
<div
class=
"space-y-3"
>
<PendingOAuthCreateAccountForm
<input
test-id-prefix=
"linuxdo"
v-model=
"pendingAccountEmail"
:initial-email=
"pendingAccountEmail"
data-testid=
"linuxdo-create-account-email"
:is-submitting=
"isSubmitting"
type=
"email"
:error-message=
"accountActionError"
class=
"input w-full"
@
submit=
"handleCreateAccount"
placeholder=
"you@example.com"
@
switch-to-bind=
"switchToBindLoginMode"
:disabled=
"isSubmitting"
/>
@
keyup.enter=
"handleCreateAccount"
/>
<button
data-testid=
"linuxdo-create-account-submit"
class=
"btn btn-primary w-full"
:disabled=
"isSubmitting || !pendingAccountEmail.trim()"
@
click=
"handleCreateAccount"
>
{{
isSubmitting
?
t
(
'
common.processing
'
)
:
'
Create account
'
}}
</button>
<button
class=
"btn btn-secondary w-full"
:disabled=
"isSubmitting"
@
click=
"switchToBindLoginMode"
>
I already have an account
</button>
</div>
<transition
name=
"fade"
>
<p
v-if=
"accountActionError"
class=
"text-sm text-red-600 dark:text-red-400"
>
{{
accountActionError
}}
</p>
</transition>
</
template
>
</
template
>
<
template
v-else-if=
"needsBindLogin"
>
<
template
v-else-if=
"needsBindLogin"
>
...
@@ -258,6 +235,9 @@ import { computed, onMounted, ref } from 'vue'
...
@@ -258,6 +235,9 @@ import { computed, onMounted, ref } from 'vue'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
PendingOAuthCreateAccountForm
,
{
type
PendingOAuthCreateAccountPayload
}
from
'
@/components/auth/PendingOAuthCreateAccountForm.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
...
@@ -432,9 +412,9 @@ function applyTotpChallenge(completion: LinuxDoPendingActionResponse): boolean {
...
@@ -432,9 +412,9 @@ function applyTotpChallenge(completion: LinuxDoPendingActionResponse): boolean {
return
true
return
true
}
}
function
switchToBindLoginMode
()
{
function
switchToBindLoginMode
(
nextEmail
?:
string
)
{
pendingAccountAction
.
value
=
'
bind_login
'
pendingAccountAction
.
value
=
'
bind_login
'
bindLoginEmail
.
value
=
bindLoginEmail
.
value
.
trim
()
||
pendingAccountEmail
.
value
.
trim
()
bindLoginEmail
.
value
=
bindLoginEmail
.
value
.
trim
()
||
nextEmail
?.
trim
()
||
pendingAccountEmail
.
value
.
trim
()
bindLoginPassword
.
value
=
''
bindLoginPassword
.
value
=
''
accountActionError
.
value
=
''
accountActionError
.
value
=
''
canReturnToCreateAccount
.
value
=
true
canReturnToCreateAccount
.
value
=
true
...
@@ -533,15 +513,16 @@ async function handleContinueLogin() {
...
@@ -533,15 +513,16 @@ async function handleContinueLogin() {
}
}
}
}
async
function
handleCreateAccount
()
{
async
function
handleCreateAccount
(
payload
:
PendingOAuthCreateAccountPayload
)
{
accountActionError
.
value
=
''
accountActionError
.
value
=
''
const
email
=
pendingAccountEmail
.
value
.
trim
()
if
(
!
payload
.
email
||
!
payload
.
password
)
return
if
(
!
email
)
return
isSubmitting
.
value
=
true
isSubmitting
.
value
=
true
try
{
try
{
const
{
data
}
=
await
apiClient
.
post
<
LinuxDoPendingActionResponse
>
(
'
/auth/oauth/pending/create-account
'
,
{
const
{
data
}
=
await
apiClient
.
post
<
LinuxDoPendingActionResponse
>
(
'
/auth/oauth/pending/create-account
'
,
{
email
,
email
:
payload
.
email
,
password
:
payload
.
password
,
verify_code
:
payload
.
verifyCode
||
undefined
,
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
})
})
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
...
...
frontend/src/views/auth/OidcCallbackView.vue
View file @
0fa47f18
...
@@ -122,37 +122,14 @@
...
@@ -122,37 +122,14 @@
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
Enter
an
email
address
to
create
your
account
and
continue
.
Enter
an
email
address
to
create
your
account
and
continue
.
<
/p
>
<
/p
>
<
div
class
=
"
space-y-3
"
>
<
PendingOAuthCreateAccountForm
<
input
test
-
id
-
prefix
=
"
oidc
"
v
-
model
=
"
pendingAccountEmail
"
:
initial
-
email
=
"
pendingAccountEmail
"
data
-
testid
=
"
oidc-create-account-email
"
:
is
-
submitting
=
"
isSubmitting
"
type
=
"
email
"
:
error
-
message
=
"
accountActionError
"
class
=
"
input w-full
"
@
submit
=
"
handleCreateAccount
"
placeholder
=
"
you@example.com
"
@
switch
-
to
-
bind
=
"
switchToBindLoginMode
"
:
disabled
=
"
isSubmitting
"
/>
@
keyup
.
enter
=
"
handleCreateAccount
"
/>
<
button
data
-
testid
=
"
oidc-create-account-submit
"
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
isSubmitting || !pendingAccountEmail.trim()
"
@
click
=
"
handleCreateAccount
"
>
{{
isSubmitting
?
t
(
'
common.processing
'
)
:
'
Create account
'
}}
<
/button
>
<
button
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToBindLoginMode
"
>
I
already
have
an
account
<
/button
>
<
/div
>
<
transition
name
=
"
fade
"
>
<
p
v
-
if
=
"
accountActionError
"
class
=
"
text-sm text-red-600 dark:text-red-400
"
>
{{
accountActionError
}}
<
/p
>
<
/transition
>
<
/template
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsBindLogin
"
>
<
template
v
-
else
-
if
=
"
needsBindLogin
"
>
...
@@ -267,6 +244,9 @@ import { computed, onMounted, ref } from 'vue'
...
@@ -267,6 +244,9 @@ import { computed, onMounted, ref } from 'vue'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
PendingOAuthCreateAccountForm
,
{
type
PendingOAuthCreateAccountPayload
}
from
'
@/components/auth/PendingOAuthCreateAccountForm.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
...
@@ -476,9 +456,9 @@ function applyTotpChallenge(completion: PendingOidcCompletion): boolean {
...
@@ -476,9 +456,9 @@ function applyTotpChallenge(completion: PendingOidcCompletion): boolean {
return
true
return
true
}
}
function
switchToBindLoginMode
()
{
function
switchToBindLoginMode
(
nextEmail
?:
string
)
{
pendingAccountAction
.
value
=
'
bind_login
'
pendingAccountAction
.
value
=
'
bind_login
'
bindLoginEmail
.
value
=
bindLoginEmail
.
value
.
trim
()
||
pendingAccountEmail
.
value
.
trim
()
bindLoginEmail
.
value
=
bindLoginEmail
.
value
.
trim
()
||
nextEmail
?.
trim
()
||
pendingAccountEmail
.
value
.
trim
()
bindLoginPassword
.
value
=
''
bindLoginPassword
.
value
=
''
accountActionError
.
value
=
''
accountActionError
.
value
=
''
canReturnToCreateAccount
.
value
=
true
canReturnToCreateAccount
.
value
=
true
...
@@ -577,15 +557,16 @@ async function handleContinueLogin() {
...
@@ -577,15 +557,16 @@ async function handleContinueLogin() {
}
}
}
}
async
function
handleCreateAccount
()
{
async
function
handleCreateAccount
(
payload
:
PendingOAuthCreateAccountPayload
)
{
accountActionError
.
value
=
''
accountActionError
.
value
=
''
const
email
=
pendingAccountEmail
.
value
.
trim
()
if
(
!
payload
.
email
||
!
payload
.
password
)
return
if
(
!
email
)
return
isSubmitting
.
value
=
true
isSubmitting
.
value
=
true
try
{
try
{
const
{
data
}
=
await
apiClient
.
post
<
PendingOidcCompletion
>
(
'
/auth/oauth/pending/create-account
'
,
{
const
{
data
}
=
await
apiClient
.
post
<
PendingOidcCompletion
>
(
'
/auth/oauth/pending/create-account
'
,
{
email
,
email
:
payload
.
email
,
password
:
payload
.
password
,
verify_code
:
payload
.
verifyCode
||
undefined
,
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
}
)
}
)
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
...
...
frontend/src/views/auth/WechatCallbackView.vue
View file @
0fa47f18
...
@@ -160,37 +160,14 @@
...
@@ -160,37 +160,14 @@
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
Enter
an
email
address
to
create
your
account
and
continue
.
Enter
an
email
address
to
create
your
account
and
continue
.
<
/p
>
<
/p
>
<
div
class
=
"
space-y-3
"
>
<
PendingOAuthCreateAccountForm
<
input
test
-
id
-
prefix
=
"
wechat
"
v
-
model
=
"
pendingAccountEmail
"
:
initial
-
email
=
"
pendingAccountEmail
"
data
-
testid
=
"
wechat-create-account-email
"
:
is
-
submitting
=
"
isSubmitting
"
type
=
"
email
"
:
error
-
message
=
"
accountActionError
"
class
=
"
input w-full
"
@
submit
=
"
handleCreateAccount
"
placeholder
=
"
you@example.com
"
@
switch
-
to
-
bind
=
"
switchToBindLoginMode
"
:
disabled
=
"
isSubmitting
"
/>
@
keyup
.
enter
=
"
handleCreateAccount
"
/>
<
button
data
-
testid
=
"
wechat-create-account-submit
"
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
isSubmitting || !pendingAccountEmail.trim()
"
@
click
=
"
handleCreateAccount
"
>
{{
isSubmitting
?
t
(
'
common.processing
'
)
:
'
Create account
'
}}
<
/button
>
<
button
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToBindLoginMode
"
>
I
already
have
an
account
<
/button
>
<
/div
>
<
transition
name
=
"
fade
"
>
<
p
v
-
if
=
"
accountActionError
"
class
=
"
text-sm text-red-600 dark:text-red-400
"
>
{{
accountActionError
}}
<
/p
>
<
/transition
>
<
/template
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsBindLogin
"
>
<
template
v
-
else
-
if
=
"
needsBindLogin
"
>
...
@@ -305,6 +282,9 @@ import { computed, onMounted, ref } from 'vue'
...
@@ -305,6 +282,9 @@ import { computed, onMounted, ref } from 'vue'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
PendingOAuthCreateAccountForm
,
{
type
PendingOAuthCreateAccountPayload
}
from
'
@/components/auth/PendingOAuthCreateAccountForm.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
...
@@ -575,9 +555,9 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
...
@@ -575,9 +555,9 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
return
true
return
true
}
}
function
switchToBindLoginMode
()
{
function
switchToBindLoginMode
(
nextEmail
?:
string
)
{
pendingAccountAction
.
value
=
'
bind_login
'
pendingAccountAction
.
value
=
'
bind_login
'
bindLoginEmail
.
value
=
bindLoginEmail
.
value
.
trim
()
||
pendingAccountEmail
.
value
.
trim
()
bindLoginEmail
.
value
=
bindLoginEmail
.
value
.
trim
()
||
nextEmail
?.
trim
()
||
pendingAccountEmail
.
value
.
trim
()
bindLoginPassword
.
value
=
''
bindLoginPassword
.
value
=
''
accountActionError
.
value
=
''
accountActionError
.
value
=
''
canReturnToCreateAccount
.
value
=
true
canReturnToCreateAccount
.
value
=
true
...
@@ -676,15 +656,16 @@ async function handleContinueLogin() {
...
@@ -676,15 +656,16 @@ async function handleContinueLogin() {
}
}
}
}
async
function
handleCreateAccount
()
{
async
function
handleCreateAccount
(
payload
:
PendingOAuthCreateAccountPayload
)
{
accountActionError
.
value
=
''
accountActionError
.
value
=
''
const
email
=
pendingAccountEmail
.
value
.
trim
()
if
(
!
payload
.
email
||
!
payload
.
password
)
return
if
(
!
email
)
return
isSubmitting
.
value
=
true
isSubmitting
.
value
=
true
try
{
try
{
const
{
data
}
=
await
apiClient
.
post
<
PendingWeChatCompletion
>
(
'
/auth/oauth/pending/create-account
'
,
{
const
{
data
}
=
await
apiClient
.
post
<
PendingWeChatCompletion
>
(
'
/auth/oauth/pending/create-account
'
,
{
email
,
email
:
payload
.
email
,
password
:
payload
.
password
,
verify_code
:
payload
.
verifyCode
||
undefined
,
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
}
)
}
)
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
...
...
frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts
View file @
0fa47f18
...
@@ -11,6 +11,7 @@ const exchangePendingOAuthCompletion = vi.fn()
...
@@ -11,6 +11,7 @@ const exchangePendingOAuthCompletion = vi.fn()
const
completeLinuxDoOAuthRegistration
=
vi
.
fn
()
const
completeLinuxDoOAuthRegistration
=
vi
.
fn
()
const
login2FA
=
vi
.
fn
()
const
login2FA
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
sendVerifyCode
=
vi
.
fn
()
vi
.
mock
(
'
vue-router
'
,
()
=>
({
vi
.
mock
(
'
vue-router
'
,
()
=>
({
useRoute
:
()
=>
({
useRoute
:
()
=>
({
...
@@ -53,7 +54,8 @@ vi.mock('@/api/auth', async () => {
...
@@ -53,7 +54,8 @@ vi.mock('@/api/auth', async () => {
...
actual
,
...
actual
,
exchangePendingOAuthCompletion
:
(...
args
:
any
[])
=>
exchangePendingOAuthCompletion
(...
args
),
exchangePendingOAuthCompletion
:
(...
args
:
any
[])
=>
exchangePendingOAuthCompletion
(...
args
),
completeLinuxDoOAuthRegistration
:
(...
args
:
any
[])
=>
completeLinuxDoOAuthRegistration
(...
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', () => {
...
@@ -67,6 +69,7 @@ describe('LinuxDoCallbackView', () => {
completeLinuxDoOAuthRegistration
.
mockReset
()
completeLinuxDoOAuthRegistration
.
mockReset
()
login2FA
.
mockReset
()
login2FA
.
mockReset
()
apiClientPost
.
mockReset
()
apiClientPost
.
mockReset
()
sendVerifyCode
.
mockReset
()
})
})
it
(
'
does not send adoption decisions during the initial exchange
'
,
async
()
=>
{
it
(
'
does not send adoption decisions during the initial exchange
'
,
async
()
=>
{
...
@@ -251,7 +254,7 @@ describe('LinuxDoCallbackView', () => {
...
@@ -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
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
redirect
:
'
/welcome
'
,
...
@@ -286,11 +289,15 @@ describe('LinuxDoCallbackView', () => {
...
@@ -286,11 +289,15 @@ describe('LinuxDoCallbackView', () => {
expect
(
checkboxes
).
toHaveLength
(
2
)
expect
(
checkboxes
).
toHaveLength
(
2
)
await
checkboxes
[
1
].
setValue
(
false
)
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-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
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
expect
(
apiClientPost
).
toHaveBeenCalledWith
(
'
/auth/oauth/pending/create-account
'
,
{
expect
(
apiClientPost
).
toHaveBeenCalledWith
(
'
/auth/oauth/pending/create-account
'
,
{
email
:
'
new@example.com
'
,
email
:
'
new@example.com
'
,
password
:
'
secret-123
'
,
verify_code
:
'
246810
'
,
adopt_display_name
:
true
,
adopt_display_name
:
true
,
adopt_avatar
:
false
adopt_avatar
:
false
})
})
...
@@ -298,6 +305,38 @@ describe('LinuxDoCallbackView', () => {
...
@@ -298,6 +305,38 @@ describe('LinuxDoCallbackView', () => {
expect
(
replace
).
toHaveBeenCalledWith
(
'
/welcome
'
)
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
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
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
()
=>
{
it
(
'
shows bind-login form for existing account binding and submits credentials with adoption decisions
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
bind_login_required
'
,
error
:
'
bind_login_required
'
,
...
...
frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts
View file @
0fa47f18
...
@@ -12,6 +12,7 @@ const completeOIDCOAuthRegistration = vi.fn()
...
@@ -12,6 +12,7 @@ const completeOIDCOAuthRegistration = vi.fn()
const
getPublicSettings
=
vi
.
fn
()
const
getPublicSettings
=
vi
.
fn
()
const
login2FA
=
vi
.
fn
()
const
login2FA
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
sendVerifyCode
=
vi
.
fn
()
vi
.
mock
(
'
vue-router
'
,
()
=>
({
vi
.
mock
(
'
vue-router
'
,
()
=>
({
useRoute
:
()
=>
({
useRoute
:
()
=>
({
...
@@ -60,7 +61,8 @@ vi.mock('@/api/auth', async () => {
...
@@ -60,7 +61,8 @@ vi.mock('@/api/auth', async () => {
exchangePendingOAuthCompletion
:
(...
args
:
any
[])
=>
exchangePendingOAuthCompletion
(...
args
),
exchangePendingOAuthCompletion
:
(...
args
:
any
[])
=>
exchangePendingOAuthCompletion
(...
args
),
completeOIDCOAuthRegistration
:
(...
args
:
any
[])
=>
completeOIDCOAuthRegistration
(...
args
),
completeOIDCOAuthRegistration
:
(...
args
:
any
[])
=>
completeOIDCOAuthRegistration
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettings
(...
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', () => {
...
@@ -75,6 +77,7 @@ describe('OidcCallbackView', () => {
getPublicSettings
.
mockReset
()
getPublicSettings
.
mockReset
()
login2FA
.
mockReset
()
login2FA
.
mockReset
()
apiClientPost
.
mockReset
()
apiClientPost
.
mockReset
()
sendVerifyCode
.
mockReset
()
getPublicSettings
.
mockResolvedValue
({
getPublicSettings
.
mockResolvedValue
({
oidc_oauth_provider_name
:
'
ExampleID
'
oidc_oauth_provider_name
:
'
ExampleID
'
})
})
...
@@ -234,7 +237,7 @@ describe('OidcCallbackView', () => {
...
@@ -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
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
redirect
:
'
/welcome
'
,
...
@@ -269,11 +272,15 @@ describe('OidcCallbackView', () => {
...
@@ -269,11 +272,15 @@ describe('OidcCallbackView', () => {
expect
(
checkboxes
).
toHaveLength
(
2
)
expect
(
checkboxes
).
toHaveLength
(
2
)
await
checkboxes
[
1
].
setValue
(
false
)
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-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
wrapper
.
get
(
'
[data-testid="oidc-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
expect
(
apiClientPost
).
toHaveBeenCalledWith
(
'
/auth/oauth/pending/create-account
'
,
{
expect
(
apiClientPost
).
toHaveBeenCalledWith
(
'
/auth/oauth/pending/create-account
'
,
{
email
:
'
new@example.com
'
,
email
:
'
new@example.com
'
,
password
:
'
secret-123
'
,
verify_code
:
'
246810
'
,
adopt_display_name
:
true
,
adopt_display_name
:
true
,
adopt_avatar
:
false
adopt_avatar
:
false
})
})
...
@@ -281,6 +288,38 @@ describe('OidcCallbackView', () => {
...
@@ -281,6 +288,38 @@ describe('OidcCallbackView', () => {
expect
(
replace
).
toHaveBeenCalledWith
(
'
/welcome
'
)
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
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
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
()
=>
{
it
(
'
shows bind-login form for existing account binding and submits credentials with adoption decisions
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
adopt_existing_user_by_email
'
,
error
:
'
adopt_existing_user_by_email
'
,
...
...
frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
View file @
0fa47f18
...
@@ -7,6 +7,7 @@ const {
...
@@ -7,6 +7,7 @@ const {
completeWeChatOAuthRegistrationMock
,
completeWeChatOAuthRegistrationMock
,
login2FAMock
,
login2FAMock
,
apiClientPostMock
,
apiClientPostMock
,
sendVerifyCodeMock
,
prepareOAuthBindAccessTokenCookieMock
,
prepareOAuthBindAccessTokenCookieMock
,
getAuthTokenMock
,
getAuthTokenMock
,
replaceMock
,
replaceMock
,
...
@@ -20,6 +21,7 @@ const {
...
@@ -20,6 +21,7 @@ const {
completeWeChatOAuthRegistrationMock
:
vi
.
fn
(),
completeWeChatOAuthRegistrationMock
:
vi
.
fn
(),
login2FAMock
:
vi
.
fn
(),
login2FAMock
:
vi
.
fn
(),
apiClientPostMock
:
vi
.
fn
(),
apiClientPostMock
:
vi
.
fn
(),
sendVerifyCodeMock
:
vi
.
fn
(),
prepareOAuthBindAccessTokenCookieMock
:
vi
.
fn
(),
prepareOAuthBindAccessTokenCookieMock
:
vi
.
fn
(),
getAuthTokenMock
:
vi
.
fn
(),
getAuthTokenMock
:
vi
.
fn
(),
replaceMock
:
vi
.
fn
(),
replaceMock
:
vi
.
fn
(),
...
@@ -118,6 +120,7 @@ vi.mock('@/api/auth', async () => {
...
@@ -118,6 +120,7 @@ vi.mock('@/api/auth', async () => {
exchangePendingOAuthCompletion
:
(...
args
:
any
[])
=>
exchangePendingOAuthCompletionMock
(...
args
),
exchangePendingOAuthCompletion
:
(...
args
:
any
[])
=>
exchangePendingOAuthCompletionMock
(...
args
),
completeWeChatOAuthRegistration
:
(...
args
:
any
[])
=>
completeWeChatOAuthRegistrationMock
(...
args
),
completeWeChatOAuthRegistration
:
(...
args
:
any
[])
=>
completeWeChatOAuthRegistrationMock
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FAMock
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FAMock
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCodeMock
(...
args
),
prepareOAuthBindAccessTokenCookie
:
(...
args
:
any
[])
=>
prepareOAuthBindAccessTokenCookieMock
(...
args
),
prepareOAuthBindAccessTokenCookie
:
(...
args
:
any
[])
=>
prepareOAuthBindAccessTokenCookieMock
(...
args
),
getAuthToken
:
(...
args
:
any
[])
=>
getAuthTokenMock
(...
args
),
getAuthToken
:
(...
args
:
any
[])
=>
getAuthTokenMock
(...
args
),
}
}
...
@@ -129,6 +132,7 @@ describe('WechatCallbackView', () => {
...
@@ -129,6 +132,7 @@ describe('WechatCallbackView', () => {
completeWeChatOAuthRegistrationMock
.
mockReset
()
completeWeChatOAuthRegistrationMock
.
mockReset
()
login2FAMock
.
mockReset
()
login2FAMock
.
mockReset
()
apiClientPostMock
.
mockReset
()
apiClientPostMock
.
mockReset
()
sendVerifyCodeMock
.
mockReset
()
replaceMock
.
mockReset
()
replaceMock
.
mockReset
()
setTokenMock
.
mockReset
()
setTokenMock
.
mockReset
()
showSuccessMock
.
mockReset
()
showSuccessMock
.
mockReset
()
...
@@ -374,7 +378,7 @@ describe('WechatCallbackView', () => {
...
@@ -374,7 +378,7 @@ describe('WechatCallbackView', () => {
expect
(
locationState
.
current
.
href
).
toContain
(
'
mode=open
'
)
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
({
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
redirect
:
'
/welcome
'
,
...
@@ -409,11 +413,15 @@ describe('WechatCallbackView', () => {
...
@@ -409,11 +413,15 @@ describe('WechatCallbackView', () => {
expect
(
checkboxes
).
toHaveLength
(
2
)
expect
(
checkboxes
).
toHaveLength
(
2
)
await
checkboxes
[
1
].
setValue
(
false
)
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-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
wrapper
.
get
(
'
[data-testid="wechat-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
expect
(
apiClientPostMock
).
toHaveBeenCalledWith
(
'
/auth/oauth/pending/create-account
'
,
{
expect
(
apiClientPostMock
).
toHaveBeenCalledWith
(
'
/auth/oauth/pending/create-account
'
,
{
email
:
'
new@example.com
'
,
email
:
'
new@example.com
'
,
password
:
'
secret-123
'
,
verify_code
:
'
246810
'
,
adopt_display_name
:
true
,
adopt_display_name
:
true
,
adopt_avatar
:
false
,
adopt_avatar
:
false
,
})
})
...
@@ -421,6 +429,38 @@ describe('WechatCallbackView', () => {
...
@@ -421,6 +429,38 @@ describe('WechatCallbackView', () => {
expect
(
replaceMock
).
toHaveBeenCalledWith
(
'
/welcome
'
)
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
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
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
()
=>
{
it
(
'
shows bind-login form for existing account binding and submits credentials with adoption decisions
'
,
async
()
=>
{
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
step
:
'
bind_login_required
'
,
step
:
'
bind_login_required
'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment