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
12f4af74
"...components/keys/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "8519a8eb560f612188638550c9346db7b8857a7f"
Commit
12f4af74
authored
Apr 21, 2026
by
IanShaw027
Browse files
fix auth pending adoption and turnstile flow
parent
e4fe9fae
Changes
6
Show whitespace changes
Inline
Side-by-side
frontend/src/components/auth/PendingOAuthCreateAccountForm.vue
View file @
12f4af74
...
@@ -16,6 +16,15 @@
...
@@ -16,6 +16,15 @@
placeholder="Password"
placeholder="Password"
:disabled="isSubmitting"
:disabled="isSubmitting"
/>
/>
<div
v-if=
"turnstileEnabled && turnstileSiteKey"
class=
"space-y-2"
>
<TurnstileWidget
ref=
"turnstileRef"
:site-key=
"turnstileSiteKey"
@
verify=
"onTurnstileVerify"
@
expire=
"onTurnstileExpire"
@
error=
"onTurnstileError"
/>
</div>
<div
class=
"flex gap-3"
>
<div
class=
"flex gap-3"
>
<input
<input
v-model=
"verifyCode"
v-model=
"verifyCode"
...
@@ -31,7 +40,7 @@
...
@@ -31,7 +40,7 @@
:data-testid=
"`$
{testIdPrefix}-create-account-send-code`"
:data-testid=
"`$
{testIdPrefix}-create-account-send-code`"
type="button"
type="button"
class="btn btn-secondary shrink-0"
class="btn btn-secondary shrink-0"
:disabled="isSubmitting || isSendingCode || countdown > 0 || !email.trim()"
:disabled="isSubmitting || isSendingCode || countdown > 0 || !email.trim()
|| (turnstileEnabled
&&
!turnstileToken)
"
@click="handleSendCode"
@click="handleSendCode"
>
>
{{
{{
...
@@ -80,9 +89,10 @@
...
@@ -80,9 +89,10 @@
<
/template
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
<
script
setup
lang
=
"
ts
"
>
import
{
onUnmounted
,
ref
,
watch
}
from
'
vue
'
import
{
onMounted
,
onUnmounted
,
ref
,
watch
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
sendVerifyCode
}
from
'
@/api/auth
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
{
getPublicSettings
,
sendVerifyCode
}
from
'
@/api/auth
'
export
type
PendingOAuthCreateAccountPayload
=
{
export
type
PendingOAuthCreateAccountPayload
=
{
email
:
string
email
:
string
...
@@ -111,6 +121,10 @@ const isSendingCode = ref(false)
...
@@ -111,6 +121,10 @@ const isSendingCode = ref(false)
const
sendCodeError
=
ref
(
''
)
const
sendCodeError
=
ref
(
''
)
const
sendCodeSuccess
=
ref
(
false
)
const
sendCodeSuccess
=
ref
(
false
)
const
countdown
=
ref
(
0
)
const
countdown
=
ref
(
0
)
const
turnstileEnabled
=
ref
(
false
)
const
turnstileSiteKey
=
ref
(
''
)
const
turnstileToken
=
ref
(
''
)
const
turnstileRef
=
ref
<
InstanceType
<
typeof
TurnstileWidget
>
|
null
>
(
null
)
let
countdownTimer
:
ReturnType
<
typeof
setInterval
>
|
null
=
null
let
countdownTimer
:
ReturnType
<
typeof
setInterval
>
|
null
=
null
...
@@ -153,22 +167,51 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -153,22 +167,51 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
}
}
function
resetTurnstile
()
{
turnstileToken
.
value
=
''
turnstileRef
.
value
?.
reset
()
}
function
onTurnstileVerify
(
token
:
string
)
{
turnstileToken
.
value
=
token
sendCodeError
.
value
=
''
}
function
onTurnstileExpire
()
{
turnstileToken
.
value
=
''
sendCodeError
.
value
=
t
(
'
auth.turnstileExpired
'
)
}
function
onTurnstileError
()
{
turnstileToken
.
value
=
''
sendCodeError
.
value
=
t
(
'
auth.turnstileFailed
'
)
}
async
function
handleSendCode
()
{
async
function
handleSendCode
()
{
const
trimmedEmail
=
email
.
value
.
trim
()
const
trimmedEmail
=
email
.
value
.
trim
()
if
(
!
trimmedEmail
)
{
if
(
!
trimmedEmail
)
{
return
return
}
}
if
(
turnstileEnabled
.
value
&&
!
turnstileToken
.
value
)
{
sendCodeError
.
value
=
t
(
'
auth.completeVerification
'
)
return
}
isSendingCode
.
value
=
true
isSendingCode
.
value
=
true
sendCodeError
.
value
=
''
sendCodeError
.
value
=
''
sendCodeSuccess
.
value
=
false
sendCodeSuccess
.
value
=
false
try
{
try
{
const
response
=
await
sendVerifyCode
({
const
response
=
await
sendVerifyCode
({
email
:
trimmedEmail
email
:
trimmedEmail
,
turnstile_token
:
turnstileEnabled
.
value
?
turnstileToken
.
value
:
undefined
}
)
}
)
sendCodeSuccess
.
value
=
true
sendCodeSuccess
.
value
=
true
startCountdown
(
response
.
countdown
)
startCountdown
(
response
.
countdown
)
if
(
turnstileEnabled
.
value
)
{
resetTurnstile
()
}
}
catch
(
error
:
unknown
)
{
}
catch
(
error
:
unknown
)
{
sendCodeError
.
value
=
getRequestErrorMessage
(
error
,
t
(
'
auth.sendCodeFailed
'
))
sendCodeError
.
value
=
getRequestErrorMessage
(
error
,
t
(
'
auth.sendCodeFailed
'
))
}
finally
{
}
finally
{
...
@@ -193,6 +236,17 @@ function emitSwitchToBind() {
...
@@ -193,6 +236,17 @@ function emitSwitchToBind() {
emit
(
'
switchToBind
'
,
email
.
value
.
trim
())
emit
(
'
switchToBind
'
,
email
.
value
.
trim
())
}
}
onMounted
(
async
()
=>
{
try
{
const
settings
=
await
getPublicSettings
()
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
===
true
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
}
catch
{
turnstileEnabled
.
value
=
false
turnstileSiteKey
.
value
=
''
}
}
)
onUnmounted
(()
=>
{
onUnmounted
(()
=>
{
clearCountdown
()
clearCountdown
()
}
)
}
)
...
...
frontend/src/components/auth/__tests__/PendingOAuthCreateAccountForm.spec.ts
View file @
12f4af74
...
@@ -4,6 +4,7 @@ import { flushPromises, mount } from '@vue/test-utils'
...
@@ -4,6 +4,7 @@ import { flushPromises, mount } from '@vue/test-utils'
import
PendingOAuthCreateAccountForm
from
'
../PendingOAuthCreateAccountForm.vue
'
import
PendingOAuthCreateAccountForm
from
'
../PendingOAuthCreateAccountForm.vue
'
const
sendVerifyCode
=
vi
.
fn
()
const
sendVerifyCode
=
vi
.
fn
()
const
getPublicSettings
=
vi
.
fn
()
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
vue-i18n
'
)
>
(
'
vue-i18n
'
)
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
vue-i18n
'
)
>
(
'
vue-i18n
'
)
...
@@ -19,13 +20,19 @@ vi.mock('@/api/auth', async () => {
...
@@ -19,13 +20,19 @@ vi.mock('@/api/auth', async () => {
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
@/api/auth
'
)
>
(
'
@/api/auth
'
)
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
@/api/auth
'
)
>
(
'
@/api/auth
'
)
return
{
return
{
...
actual
,
...
actual
,
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
)
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettings
(...
args
)
}
}
})
})
describe
(
'
PendingOAuthCreateAccountForm
'
,
()
=>
{
describe
(
'
PendingOAuthCreateAccountForm
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
sendVerifyCode
.
mockReset
()
sendVerifyCode
.
mockReset
()
getPublicSettings
.
mockReset
()
getPublicSettings
.
mockResolvedValue
({
turnstile_enabled
:
false
,
turnstile_site_key
:
''
})
})
})
it
(
'
emits trimmed email, password, and verify code on submit
'
,
async
()
=>
{
it
(
'
emits trimmed email, password, and verify code on submit
'
,
async
()
=>
{
...
@@ -77,4 +84,45 @@ describe('PendingOAuthCreateAccountForm', () => {
...
@@ -77,4 +84,45 @@ describe('PendingOAuthCreateAccountForm', () => {
email
:
'
user@example.com
'
email
:
'
user@example.com
'
})
})
})
})
it
(
'
requires a turnstile token before sending a verify code when turnstile is enabled
'
,
async
()
=>
{
getPublicSettings
.
mockResolvedValue
({
turnstile_enabled
:
true
,
turnstile_site_key
:
'
site-key
'
})
sendVerifyCode
.
mockResolvedValue
({
message
:
'
sent
'
,
countdown
:
60
})
const
wrapper
=
mount
(
PendingOAuthCreateAccountForm
,
{
props
:
{
providerName
:
'
LinuxDo
'
,
testIdPrefix
:
'
linuxdo
'
,
initialEmail
:
''
,
isSubmitting
:
false
},
global
:
{
stubs
:
{
TurnstileWidget
:
{
template
:
'
<button data-testid="turnstile-verify" @click="$emit(
\'
verify
\'
,
\'
turnstile-token
\'
)">verify</button>
'
}
}
}
})
await
flushPromises
()
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-email"]
'
).
setValue
(
'
user@example.com
'
)
expect
(
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-send-code"]
'
).
attributes
(
'
disabled
'
)).
toBeDefined
()
await
wrapper
.
get
(
'
[data-testid="turnstile-verify"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-send-code"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
expect
(
sendVerifyCode
).
toHaveBeenCalledWith
({
email
:
'
user@example.com
'
,
turnstile_token
:
'
turnstile-token
'
})
})
})
})
frontend/src/views/auth/LinuxDoCallbackView.vue
View file @
12f4af74
...
@@ -296,6 +296,19 @@ type LinuxDoPendingActionResponse = PendingOAuthExchangeResponse & {
...
@@ -296,6 +296,19 @@ type LinuxDoPendingActionResponse = PendingOAuthExchangeResponse & {
resolved_email
?:
string
resolved_email
?:
string
}
}
function
persistPendingAuthSession
(
redirect
?:
string
)
{
authStore
.
setPendingAuthSession
({
token
:
''
,
token_field
:
'
pending_oauth_token
'
,
provider
:
'
linuxdo
'
,
redirect
:
sanitizeRedirectPath
(
redirect
||
redirectTo
.
value
)
})
}
function
clearPendingAuthSession
()
{
authStore
.
clearPendingAuthSession
()
}
function
parseFragmentParams
():
URLSearchParams
{
function
parseFragmentParams
():
URLSearchParams
{
const
raw
=
typeof
window
!==
'
undefined
'
?
window
.
location
.
hash
:
''
const
raw
=
typeof
window
!==
'
undefined
'
?
window
.
location
.
hash
:
''
const
hash
=
raw
.
startsWith
(
'
#
'
)
?
raw
.
slice
(
1
)
:
raw
const
hash
=
raw
.
startsWith
(
'
#
'
)
?
raw
.
slice
(
1
)
:
raw
...
@@ -434,6 +447,7 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -434,6 +447,7 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
clearPendingAuthSession
()
appStore
.
showSuccess
(
bindSuccessMessage
)
appStore
.
showSuccess
(
bindSuccessMessage
)
await
router
.
replace
(
bindRedirect
)
await
router
.
replace
(
bindRedirect
)
return
return
...
@@ -451,16 +465,19 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
...
@@ -451,16 +465,19 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
async
function
finalizePendingAccountResponse
(
completion
:
LinuxDoPendingActionResponse
)
{
async
function
finalizePendingAccountResponse
(
completion
:
LinuxDoPendingActionResponse
)
{
applyAdoptionSuggestionState
(
completion
)
applyAdoptionSuggestionState
(
completion
)
const
redirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
redirectTo
.
value
)
if
(
completion
.
error
===
'
invitation_required
'
)
{
if
(
completion
.
error
===
'
invitation_required
'
)
{
pendingAccountAction
.
value
=
'
none
'
pendingAccountAction
.
value
=
'
none
'
needsInvitation
.
value
=
true
needsInvitation
.
value
=
true
needsAdoptionConfirmation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
if
(
applyTotpChallenge
(
completion
))
{
if
(
applyTotpChallenge
(
completion
))
{
persistPendingAuthSession
(
redirect
)
return
return
}
}
...
@@ -469,10 +486,10 @@ async function finalizePendingAccountResponse(completion: LinuxDoPendingActionRe
...
@@ -469,10 +486,10 @@ async function finalizePendingAccountResponse(completion: LinuxDoPendingActionRe
needsInvitation
.
value
=
false
needsInvitation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
const
redirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
redirectTo
.
value
)
await
finalizeCompletion
(
completion
,
redirect
)
await
finalizeCompletion
(
completion
,
redirect
)
}
}
...
@@ -502,8 +519,8 @@ async function handleSubmitInvitation() {
...
@@ -502,8 +519,8 @@ async function handleSubmitInvitation() {
async
function
handleContinueLogin
()
{
async
function
handleContinueLogin
()
{
isSubmitting
.
value
=
true
isSubmitting
.
value
=
true
try
{
try
{
const
completion
=
await
exchangePendingOAuthCompletion
(
currentAdoptionDecision
())
const
completion
=
await
exchangePendingOAuthCompletion
(
currentAdoptionDecision
())
as
LinuxDoPendingActionResponse
await
finalize
Completion
(
completion
,
redirectTo
.
value
)
await
finalize
PendingAccountResponse
(
completion
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
appStore
.
showError
(
errorMessage
.
value
)
appStore
.
showError
(
errorMessage
.
value
)
...
@@ -598,27 +615,32 @@ onMounted(async () => {
...
@@ -598,27 +615,32 @@ onMounted(async () => {
if
(
completion
.
error
===
'
invitation_required
'
)
{
if
(
completion
.
error
===
'
invitation_required
'
)
{
needsInvitation
.
value
=
true
needsInvitation
.
value
=
true
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
if
(
applyTotpChallenge
(
completion
as
LinuxDoPendingActionResponse
))
{
if
(
applyTotpChallenge
(
completion
as
LinuxDoPendingActionResponse
))
{
persistPendingAuthSession
(
redirect
)
return
return
}
}
applyPendingAccountAction
(
completion
as
LinuxDoPendingActionResponse
)
applyPendingAccountAction
(
completion
as
LinuxDoPendingActionResponse
)
if
(
pendingAccountAction
.
value
!==
'
none
'
)
{
if
(
pendingAccountAction
.
value
!==
'
none
'
)
{
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
if
(
adoptionRequired
.
value
&&
hasSuggestedProfile
(
completion
))
{
if
(
adoptionRequired
.
value
&&
hasSuggestedProfile
(
completion
))
{
needsAdoptionConfirmation
.
value
=
true
needsAdoptionConfirmation
.
value
=
true
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
await
finalizeCompletion
(
completion
,
redirect
)
await
finalizeCompletion
(
completion
,
redirect
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
clearPendingAuthSession
()
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
appStore
.
showError
(
errorMessage
.
value
)
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
isProcessing
.
value
=
false
...
...
frontend/src/views/auth/OidcCallbackView.vue
View file @
12f4af74
...
@@ -312,6 +312,19 @@ type PendingOidcCompletion = PendingOAuthExchangeResponse & {
...
@@ -312,6 +312,19 @@ type PendingOidcCompletion = PendingOAuthExchangeResponse & {
user_email_masked
?:
string
user_email_masked
?:
string
}
}
function
persistPendingAuthSession
(
redirect
?:
string
)
{
authStore
.
setPendingAuthSession
({
token
:
''
,
token_field
:
'
pending_oauth_token
'
,
provider
:
'
oidc
'
,
redirect
:
sanitizeRedirectPath
(
redirect
||
redirectTo
.
value
)
}
)
}
function
clearPendingAuthSession
()
{
authStore
.
clearPendingAuthSession
()
}
function
parseFragmentParams
():
URLSearchParams
{
function
parseFragmentParams
():
URLSearchParams
{
const
raw
=
typeof
window
!==
'
undefined
'
?
window
.
location
.
hash
:
''
const
raw
=
typeof
window
!==
'
undefined
'
?
window
.
location
.
hash
:
''
const
hash
=
raw
.
startsWith
(
'
#
'
)
?
raw
.
slice
(
1
)
:
raw
const
hash
=
raw
.
startsWith
(
'
#
'
)
?
raw
.
slice
(
1
)
:
raw
...
@@ -478,6 +491,7 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -478,6 +491,7 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
clearPendingAuthSession
()
appStore
.
showSuccess
(
bindSuccessMessage
)
appStore
.
showSuccess
(
bindSuccessMessage
)
await
router
.
replace
(
bindRedirect
)
await
router
.
replace
(
bindRedirect
)
return
return
...
@@ -495,16 +509,19 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
...
@@ -495,16 +509,19 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
async
function
finalizePendingAccountResponse
(
completion
:
PendingOidcCompletion
)
{
async
function
finalizePendingAccountResponse
(
completion
:
PendingOidcCompletion
)
{
applyAdoptionSuggestionState
(
completion
)
applyAdoptionSuggestionState
(
completion
)
const
redirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
redirectTo
.
value
)
if
(
completion
.
error
===
'
invitation_required
'
)
{
if
(
completion
.
error
===
'
invitation_required
'
)
{
pendingAccountAction
.
value
=
'
none
'
pendingAccountAction
.
value
=
'
none
'
needsInvitation
.
value
=
true
needsInvitation
.
value
=
true
needsAdoptionConfirmation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
if
(
applyTotpChallenge
(
completion
))
{
if
(
applyTotpChallenge
(
completion
))
{
persistPendingAuthSession
(
redirect
)
return
return
}
}
...
@@ -513,10 +530,10 @@ async function finalizePendingAccountResponse(completion: PendingOidcCompletion)
...
@@ -513,10 +530,10 @@ async function finalizePendingAccountResponse(completion: PendingOidcCompletion)
needsInvitation
.
value
=
false
needsInvitation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
const
redirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
redirectTo
.
value
)
await
finalizeCompletion
(
completion
,
redirect
)
await
finalizeCompletion
(
completion
,
redirect
)
}
}
...
@@ -546,8 +563,8 @@ async function handleSubmitInvitation() {
...
@@ -546,8 +563,8 @@ async function handleSubmitInvitation() {
async
function
handleContinueLogin
()
{
async
function
handleContinueLogin
()
{
isSubmitting
.
value
=
true
isSubmitting
.
value
=
true
try
{
try
{
const
completion
=
await
exchangePendingOAuthCompletion
(
currentAdoptionDecision
())
const
completion
=
await
exchangePendingOAuthCompletion
(
currentAdoptionDecision
())
as
PendingOidcCompletion
await
finalize
Completion
(
completion
,
redirectTo
.
value
)
await
finalize
PendingAccountResponse
(
completion
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
appStore
.
showError
(
errorMessage
.
value
)
appStore
.
showError
(
errorMessage
.
value
)
...
@@ -644,27 +661,32 @@ onMounted(async () => {
...
@@ -644,27 +661,32 @@ onMounted(async () => {
if
(
completion
.
error
===
'
invitation_required
'
)
{
if
(
completion
.
error
===
'
invitation_required
'
)
{
needsInvitation
.
value
=
true
needsInvitation
.
value
=
true
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
if
(
applyTotpChallenge
(
completion
))
{
if
(
applyTotpChallenge
(
completion
))
{
persistPendingAuthSession
(
redirect
)
return
return
}
}
applyPendingAccountAction
(
completion
)
applyPendingAccountAction
(
completion
)
if
(
pendingAccountAction
.
value
!==
'
none
'
)
{
if
(
pendingAccountAction
.
value
!==
'
none
'
)
{
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
if
(
adoptionRequired
.
value
&&
hasSuggestedProfile
(
completion
))
{
if
(
adoptionRequired
.
value
&&
hasSuggestedProfile
(
completion
))
{
needsAdoptionConfirmation
.
value
=
true
needsAdoptionConfirmation
.
value
=
true
isProcessing
.
value
=
false
isProcessing
.
value
=
false
persistPendingAuthSession
(
redirect
)
return
return
}
}
await
finalizeCompletion
(
completion
,
redirect
)
await
finalizeCompletion
(
completion
,
redirect
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
clearPendingAuthSession
()
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
errorMessage
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
appStore
.
showError
(
errorMessage
.
value
)
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
isProcessing
.
value
=
false
...
...
frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts
View file @
12f4af74
...
@@ -7,8 +7,11 @@ const replace = vi.fn()
...
@@ -7,8 +7,11 @@ const replace = vi.fn()
const
showSuccess
=
vi
.
fn
()
const
showSuccess
=
vi
.
fn
()
const
showError
=
vi
.
fn
()
const
showError
=
vi
.
fn
()
const
setToken
=
vi
.
fn
()
const
setToken
=
vi
.
fn
()
const
setPendingAuthSession
=
vi
.
fn
()
const
clearPendingAuthSession
=
vi
.
fn
()
const
exchangePendingOAuthCompletion
=
vi
.
fn
()
const
exchangePendingOAuthCompletion
=
vi
.
fn
()
const
completeLinuxDoOAuthRegistration
=
vi
.
fn
()
const
completeLinuxDoOAuthRegistration
=
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
()
const
sendVerifyCode
=
vi
.
fn
()
...
@@ -34,7 +37,9 @@ vi.mock('vue-i18n', async () => {
...
@@ -34,7 +37,9 @@ vi.mock('vue-i18n', async () => {
vi
.
mock
(
'
@/stores
'
,
()
=>
({
vi
.
mock
(
'
@/stores
'
,
()
=>
({
useAuthStore
:
()
=>
({
useAuthStore
:
()
=>
({
setToken
setToken
,
setPendingAuthSession
,
clearPendingAuthSession
}),
}),
useAppStore
:
()
=>
({
useAppStore
:
()
=>
({
showSuccess
,
showSuccess
,
...
@@ -54,6 +59,7 @@ vi.mock('@/api/auth', async () => {
...
@@ -54,6 +59,7 @@ 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
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettings
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FA
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FA
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
)
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
)
}
}
...
@@ -65,11 +71,18 @@ describe('LinuxDoCallbackView', () => {
...
@@ -65,11 +71,18 @@ describe('LinuxDoCallbackView', () => {
showSuccess
.
mockReset
()
showSuccess
.
mockReset
()
showError
.
mockReset
()
showError
.
mockReset
()
setToken
.
mockReset
()
setToken
.
mockReset
()
setPendingAuthSession
.
mockReset
()
clearPendingAuthSession
.
mockReset
()
exchangePendingOAuthCompletion
.
mockReset
()
exchangePendingOAuthCompletion
.
mockReset
()
completeLinuxDoOAuthRegistration
.
mockReset
()
completeLinuxDoOAuthRegistration
.
mockReset
()
getPublicSettings
.
mockReset
()
login2FA
.
mockReset
()
login2FA
.
mockReset
()
apiClientPost
.
mockReset
()
apiClientPost
.
mockReset
()
sendVerifyCode
.
mockReset
()
sendVerifyCode
.
mockReset
()
getPublicSettings
.
mockResolvedValue
({
turnstile_enabled
:
false
,
turnstile_site_key
:
''
})
})
})
it
(
'
does not send adoption decisions during the initial exchange
'
,
async
()
=>
{
it
(
'
does not send adoption decisions during the initial exchange
'
,
async
()
=>
{
...
@@ -208,6 +221,72 @@ describe('LinuxDoCallbackView', () => {
...
@@ -208,6 +221,72 @@ describe('LinuxDoCallbackView', () => {
expect
(
replace
).
toHaveBeenCalledWith
(
'
/profile/security
'
)
expect
(
replace
).
toHaveBeenCalledWith
(
'
/profile/security
'
)
})
})
it
(
'
keeps rendering pending bind-login UI when adoption confirmation leads to another pending step
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValueOnce
({
redirect
:
'
/profile/security
'
,
adoption_required
:
true
,
suggested_display_name
:
'
LinuxDo Nick
'
,
suggested_avatar_url
:
'
https://cdn.example/linuxdo.png
'
})
.
mockResolvedValueOnce
({
step
:
'
bind_login_required
'
,
redirect
:
'
/profile/security
'
,
email
:
'
existing@example.com
'
,
adoption_required
:
true
,
suggested_display_name
:
'
LinuxDo Nick
'
,
suggested_avatar_url
:
'
https://cdn.example/linuxdo.png
'
})
const
wrapper
=
mount
(
LinuxDoCallbackView
,
{
global
:
{
stubs
:
{
AuthLayout
:
{
template
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
transition
:
false
}
}
})
await
flushPromises
()
await
wrapper
.
findAll
(
'
button
'
)[
0
].
trigger
(
'
click
'
)
await
flushPromises
()
expect
(
showSuccess
).
not
.
toHaveBeenCalled
()
expect
(
replace
).
not
.
toHaveBeenCalled
()
expect
((
wrapper
.
get
(
'
[data-testid="linuxdo-bind-login-email"]
'
).
element
as
HTMLInputElement
).
value
).
toBe
(
'
existing@example.com
'
)
})
it
(
'
persists a pending auth session when the oauth flow still needs account creation
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
})
mount
(
LinuxDoCallbackView
,
{
global
:
{
stubs
:
{
AuthLayout
:
{
template
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
transition
:
false
}
}
})
await
flushPromises
()
expect
(
setPendingAuthSession
).
toHaveBeenCalledWith
({
token
:
''
,
token_field
:
'
pending_oauth_token
'
,
provider
:
'
linuxdo
'
,
redirect
:
'
/welcome
'
})
})
it
(
'
renders adoption choices for invitation flow and submits the selected values
'
,
async
()
=>
{
it
(
'
renders adoption choices for invitation flow and submits the selected values
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
invitation_required
'
,
error
:
'
invitation_required
'
,
...
...
frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts
View file @
12f4af74
...
@@ -7,6 +7,8 @@ const replace = vi.fn()
...
@@ -7,6 +7,8 @@ const replace = vi.fn()
const
showSuccess
=
vi
.
fn
()
const
showSuccess
=
vi
.
fn
()
const
showError
=
vi
.
fn
()
const
showError
=
vi
.
fn
()
const
setToken
=
vi
.
fn
()
const
setToken
=
vi
.
fn
()
const
setPendingAuthSession
=
vi
.
fn
()
const
clearPendingAuthSession
=
vi
.
fn
()
const
exchangePendingOAuthCompletion
=
vi
.
fn
()
const
exchangePendingOAuthCompletion
=
vi
.
fn
()
const
completeOIDCOAuthRegistration
=
vi
.
fn
()
const
completeOIDCOAuthRegistration
=
vi
.
fn
()
const
getPublicSettings
=
vi
.
fn
()
const
getPublicSettings
=
vi
.
fn
()
...
@@ -40,7 +42,9 @@ vi.mock('vue-i18n', async () => {
...
@@ -40,7 +42,9 @@ vi.mock('vue-i18n', async () => {
vi
.
mock
(
'
@/stores
'
,
()
=>
({
vi
.
mock
(
'
@/stores
'
,
()
=>
({
useAuthStore
:
()
=>
({
useAuthStore
:
()
=>
({
setToken
setToken
,
setPendingAuthSession
,
clearPendingAuthSession
}),
}),
useAppStore
:
()
=>
({
useAppStore
:
()
=>
({
showSuccess
,
showSuccess
,
...
@@ -72,6 +76,8 @@ describe('OidcCallbackView', () => {
...
@@ -72,6 +76,8 @@ describe('OidcCallbackView', () => {
showSuccess
.
mockReset
()
showSuccess
.
mockReset
()
showError
.
mockReset
()
showError
.
mockReset
()
setToken
.
mockReset
()
setToken
.
mockReset
()
setPendingAuthSession
.
mockReset
()
clearPendingAuthSession
.
mockReset
()
exchangePendingOAuthCompletion
.
mockReset
()
exchangePendingOAuthCompletion
.
mockReset
()
completeOIDCOAuthRegistration
.
mockReset
()
completeOIDCOAuthRegistration
.
mockReset
()
getPublicSettings
.
mockReset
()
getPublicSettings
.
mockReset
()
...
@@ -79,7 +85,9 @@ describe('OidcCallbackView', () => {
...
@@ -79,7 +85,9 @@ describe('OidcCallbackView', () => {
apiClientPost
.
mockReset
()
apiClientPost
.
mockReset
()
sendVerifyCode
.
mockReset
()
sendVerifyCode
.
mockReset
()
getPublicSettings
.
mockResolvedValue
({
getPublicSettings
.
mockResolvedValue
({
oidc_oauth_provider_name
:
'
ExampleID
'
oidc_oauth_provider_name
:
'
ExampleID
'
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
})
})
})
})
...
@@ -196,6 +204,72 @@ describe('OidcCallbackView', () => {
...
@@ -196,6 +204,72 @@ describe('OidcCallbackView', () => {
expect
(
replace
).
toHaveBeenCalledWith
(
'
/profile
'
)
expect
(
replace
).
toHaveBeenCalledWith
(
'
/profile
'
)
})
})
it
(
'
keeps rendering pending bind-login UI when adoption confirmation leads to another pending step
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValueOnce
({
redirect
:
'
/profile
'
,
adoption_required
:
true
,
suggested_display_name
:
'
OIDC Nick
'
,
suggested_avatar_url
:
'
https://cdn.example/oidc.png
'
})
.
mockResolvedValueOnce
({
step
:
'
bind_login_required
'
,
redirect
:
'
/profile
'
,
email
:
'
existing@example.com
'
,
adoption_required
:
true
,
suggested_display_name
:
'
OIDC Nick
'
,
suggested_avatar_url
:
'
https://cdn.example/oidc.png
'
})
const
wrapper
=
mount
(
OidcCallbackView
,
{
global
:
{
stubs
:
{
AuthLayout
:
{
template
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
transition
:
false
}
}
})
await
flushPromises
()
await
wrapper
.
findAll
(
'
button
'
)[
0
].
trigger
(
'
click
'
)
await
flushPromises
()
expect
(
showSuccess
).
not
.
toHaveBeenCalled
()
expect
(
replace
).
not
.
toHaveBeenCalled
()
expect
((
wrapper
.
get
(
'
[data-testid="oidc-bind-login-email"]
'
).
element
as
HTMLInputElement
).
value
).
toBe
(
'
existing@example.com
'
)
})
it
(
'
persists a pending auth session when the oauth flow still needs account creation
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
})
mount
(
OidcCallbackView
,
{
global
:
{
stubs
:
{
AuthLayout
:
{
template
:
'
<div><slot /></div>
'
},
Icon
:
true
,
RouterLink
:
{
template
:
'
<a><slot /></a>
'
},
transition
:
false
}
}
})
await
flushPromises
()
expect
(
setPendingAuthSession
).
toHaveBeenCalledWith
({
token
:
''
,
token_field
:
'
pending_oauth_token
'
,
provider
:
'
oidc
'
,
redirect
:
'
/welcome
'
})
})
it
(
'
renders adoption choices for invitation flow and submits the selected values
'
,
async
()
=>
{
it
(
'
renders adoption choices for invitation flow and submits the selected values
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
invitation_required
'
,
error
:
'
invitation_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