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
4c21320d
Commit
4c21320d
authored
Apr 21, 2026
by
IanShaw027
Browse files
fix(auth): require explicit choice for third-party signup
parent
2cebb0dc
Changes
8
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/auth_linuxdo_oauth.go
View file @
4c21320d
...
@@ -325,80 +325,18 @@ func (h *AuthHandler) LinuxDoOAuthCallback(c *gin.Context) {
...
@@ -325,80 +325,18 @@ func (h *AuthHandler) LinuxDoOAuthCallback(c *gin.Context) {
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
infraerrors
.
Reason
(
err
),
infraerrors
.
Message
(
err
))
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
infraerrors
.
Reason
(
err
),
infraerrors
.
Message
(
err
))
return
return
}
}
if
compatEmailUser
!=
nil
{
if
err
:=
h
.
createLinuxDoOAuthChoicePendingSession
(
if
err
:=
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
c
,
Intent
:
"adopt_existing_user_by_email"
,
identityKey
,
Identity
:
identityKey
,
email
,
TargetUserID
:
&
compatEmailUser
.
ID
,
email
,
ResolvedEmail
:
compatEmailUser
.
Email
,
redirectTo
,
RedirectTo
:
redirectTo
,
browserSessionKey
,
BrowserSessionKey
:
browserSessionKey
,
upstreamClaims
,
UpstreamIdentityClaims
:
upstreamClaims
,
compatEmail
,
CompletionResponse
:
map
[
string
]
any
{
compatEmailUser
,
"redirect"
:
redirectTo
,
h
.
isForceEmailOnThirdPartySignup
(
c
.
Request
.
Context
()),
"step"
:
"bind_login_required"
,
);
err
!=
nil
{
"email"
:
compatEmailUser
.
Email
,
},
});
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
}
redirectToFrontendCallback
(
c
,
frontendCallback
)
return
}
if
h
.
isForceEmailOnThirdPartySignup
(
c
.
Request
.
Context
())
{
if
err
:=
h
.
createOAuthEmailRequiredPendingSession
(
c
,
identityKey
,
redirectTo
,
browserSessionKey
,
upstreamClaims
);
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
}
redirectToFrontendCallback
(
c
,
frontendCallback
)
return
}
// 传入空邀请码;如果需要邀请码,服务层返回 ErrOAuthInvitationRequired
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
""
)
if
err
!=
nil
{
if
errors
.
Is
(
err
,
service
.
ErrOAuthInvitationRequired
)
{
if
err
:=
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
Intent
:
"login"
,
Identity
:
identityKey
,
ResolvedEmail
:
email
,
RedirectTo
:
redirectTo
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
map
[
string
]
any
{
"error"
:
"invitation_required"
,
"redirect"
:
redirectTo
,
},
});
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
}
redirectToFrontendCallback
(
c
,
frontendCallback
)
return
}
// 避免把内部细节泄露给客户端;给前端保留结构化原因与提示信息即可。
redirectOAuthError
(
c
,
frontendCallback
,
"login_failed"
,
infraerrors
.
Reason
(
err
),
infraerrors
.
Message
(
err
))
return
}
if
err
:=
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
Intent
:
"login"
,
Identity
:
identityKey
,
TargetUserID
:
&
user
.
ID
,
ResolvedEmail
:
email
,
RedirectTo
:
redirectTo
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
map
[
string
]
any
{
"access_token"
:
tokenPair
.
AccessToken
,
"refresh_token"
:
tokenPair
.
RefreshToken
,
"expires_in"
:
tokenPair
.
ExpiresIn
,
"token_type"
:
"Bearer"
,
"redirect"
:
redirectTo
,
},
});
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
return
}
}
...
@@ -431,6 +369,62 @@ func (h *AuthHandler) findLinuxDoCompatEmailUser(ctx context.Context, email stri
...
@@ -431,6 +369,62 @@ func (h *AuthHandler) findLinuxDoCompatEmailUser(ctx context.Context, email stri
return
userEntity
,
nil
return
userEntity
,
nil
}
}
func
(
h
*
AuthHandler
)
createLinuxDoOAuthChoicePendingSession
(
c
*
gin
.
Context
,
identity
service
.
PendingAuthIdentityKey
,
suggestedEmail
string
,
resolvedEmail
string
,
redirectTo
string
,
browserSessionKey
string
,
upstreamClaims
map
[
string
]
any
,
compatEmail
string
,
compatEmailUser
*
dbent
.
User
,
forceEmailOnSignup
bool
,
)
error
{
suggestionEmail
:=
strings
.
TrimSpace
(
suggestedEmail
)
canonicalEmail
:=
strings
.
TrimSpace
(
resolvedEmail
)
if
suggestionEmail
==
""
{
suggestionEmail
=
canonicalEmail
}
completionResponse
:=
map
[
string
]
any
{
"step"
:
oauthPendingChoiceStep
,
"adoption_required"
:
true
,
"redirect"
:
strings
.
TrimSpace
(
redirectTo
),
"email"
:
suggestionEmail
,
"resolved_email"
:
canonicalEmail
,
"existing_account_email"
:
""
,
"existing_account_bindable"
:
false
,
"create_account_allowed"
:
true
,
"force_email_on_signup"
:
forceEmailOnSignup
,
"choice_reason"
:
"third_party_signup"
,
}
if
strings
.
TrimSpace
(
compatEmail
)
!=
""
{
completionResponse
[
"compat_email"
]
=
strings
.
TrimSpace
(
compatEmail
)
}
resolvedChoiceEmail
:=
suggestionEmail
if
compatEmailUser
!=
nil
{
completionResponse
[
"email"
]
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
completionResponse
[
"existing_account_email"
]
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
completionResponse
[
"existing_account_bindable"
]
=
true
completionResponse
[
"choice_reason"
]
=
"compat_email_match"
resolvedChoiceEmail
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
}
if
forceEmailOnSignup
&&
compatEmailUser
==
nil
{
completionResponse
[
"choice_reason"
]
=
"force_email_on_signup"
}
return
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
Intent
:
oauthIntentLogin
,
Identity
:
identity
,
ResolvedEmail
:
resolvedChoiceEmail
,
RedirectTo
:
redirectTo
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
completionResponse
,
})
}
type
completeLinuxDoOAuthRequest
struct
{
type
completeLinuxDoOAuthRequest
struct
{
InvitationCode
string
`json:"invitation_code" binding:"required"`
InvitationCode
string
`json:"invitation_code" binding:"required"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
...
...
backend/internal/handler/auth_oauth_pending_flow.go
View file @
4c21320d
...
@@ -32,6 +32,7 @@ const (
...
@@ -32,6 +32,7 @@ const (
oauthPendingSessionCookiePath
=
"/api/v1/auth/oauth"
oauthPendingSessionCookiePath
=
"/api/v1/auth/oauth"
oauthPendingSessionCookieName
=
"oauth_pending_session"
oauthPendingSessionCookieName
=
"oauth_pending_session"
oauthPendingCookieMaxAgeSec
=
10
*
60
oauthPendingCookieMaxAgeSec
=
10
*
60
oauthPendingChoiceStep
=
"choose_account_action_required"
oauthCompletionResponseKey
=
"completion_response"
oauthCompletionResponseKey
=
"completion_response"
)
)
...
@@ -431,8 +432,9 @@ func (h *AuthHandler) createOAuthEmailRequiredPendingSession(
...
@@ -431,8 +432,9 @@ func (h *AuthHandler) createOAuthEmailRequiredPendingSession(
BrowserSessionKey
:
browserSessionKey
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
map
[
string
]
any
{
CompletionResponse
:
map
[
string
]
any
{
"redirect"
:
redirectTo
,
"redirect"
:
strings
.
TrimSpace
(
redirectTo
),
"step"
:
"email_required"
,
"step"
:
oauthPendingChoiceStep
,
"adoption_required"
:
true
,
"force_email_on_signup"
:
true
,
"force_email_on_signup"
:
true
,
"email_binding_required"
:
true
,
"email_binding_required"
:
true
,
"existing_account_bindable"
:
true
,
"existing_account_bindable"
:
true
,
...
@@ -492,7 +494,7 @@ func (h *AuthHandler) SendPendingOAuthVerifyCode(c *gin.Context) {
...
@@ -492,7 +494,7 @@ func (h *AuthHandler) SendPendingOAuthVerifyCode(c *gin.Context) {
email
:=
strings
.
TrimSpace
(
strings
.
ToLower
(
req
.
Email
))
email
:=
strings
.
TrimSpace
(
strings
.
ToLower
(
req
.
Email
))
if
existingUser
,
err
:=
findUserByNormalizedEmail
(
c
.
Request
.
Context
(),
client
,
email
);
err
==
nil
&&
existingUser
!=
nil
{
if
existingUser
,
err
:=
findUserByNormalizedEmail
(
c
.
Request
.
Context
(),
client
,
email
);
err
==
nil
&&
existingUser
!=
nil
{
session
,
err
=
h
.
transitionPendingOAuthAccountTo
BindLogin
(
c
,
client
,
session
,
email
,
oauthAdoptionDecisionRequest
{}
)
session
,
err
=
h
.
transitionPendingOAuthAccountTo
ChoiceState
(
c
,
client
,
session
,
email
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
@@ -1206,12 +1208,13 @@ func readPendingOAuthBrowserSession(c *gin.Context, h *AuthHandler) (*service.Au
...
@@ -1206,12 +1208,13 @@ func readPendingOAuthBrowserSession(c *gin.Context, h *AuthHandler) (*service.Au
}
}
func
buildPendingOAuthSessionStatusPayload
(
session
*
dbent
.
PendingAuthSession
)
gin
.
H
{
func
buildPendingOAuthSessionStatusPayload
(
session
*
dbent
.
PendingAuthSession
)
gin
.
H
{
completionResponse
:=
normalizePendingOAuthCompletionResponse
(
mergePendingCompletionResponse
(
session
,
nil
))
payload
:=
gin
.
H
{
payload
:=
gin
.
H
{
"auth_result"
:
"pending_session"
,
"auth_result"
:
"pending_session"
,
"provider"
:
strings
.
TrimSpace
(
session
.
ProviderType
),
"provider"
:
strings
.
TrimSpace
(
session
.
ProviderType
),
"intent"
:
strings
.
TrimSpace
(
session
.
Intent
),
"intent"
:
strings
.
TrimSpace
(
session
.
Intent
),
}
}
for
key
,
value
:=
range
mergePendingC
ompletionResponse
(
session
,
nil
)
{
for
key
,
value
:=
range
c
ompletionResponse
{
payload
[
key
]
=
value
payload
[
key
]
=
value
}
}
if
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
);
email
!=
""
{
if
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
);
email
!=
""
{
...
@@ -1220,38 +1223,58 @@ func buildPendingOAuthSessionStatusPayload(session *dbent.PendingAuthSession) gi
...
@@ -1220,38 +1223,58 @@ func buildPendingOAuthSessionStatusPayload(session *dbent.PendingAuthSession) gi
return
payload
return
payload
}
}
func
(
h
*
AuthHandler
)
transitionPendingOAuthAccountToBindLogin
(
func
normalizePendingOAuthCompletionResponse
(
payload
map
[
string
]
any
)
map
[
string
]
any
{
normalized
:=
clonePendingMap
(
payload
)
step
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
pendingSessionStringValue
(
normalized
,
"step"
)))
switch
step
{
case
"choice"
,
"choose_account_action"
,
"choose_account"
,
"choose"
,
"email_required"
,
"bind_login_required"
:
normalized
[
"step"
]
=
oauthPendingChoiceStep
}
if
strings
.
EqualFold
(
strings
.
TrimSpace
(
pendingSessionStringValue
(
normalized
,
"step"
)),
oauthPendingChoiceStep
)
{
normalized
[
"adoption_required"
]
=
true
}
if
_
,
exists
:=
normalized
[
"adoption_required"
];
!
exists
{
if
_
,
hasChoiceFields
:=
normalized
[
"email_binding_required"
];
hasChoiceFields
{
normalized
[
"adoption_required"
]
=
true
}
}
return
normalized
}
func
pendingOAuthChoiceCompletionResponse
(
session
*
dbent
.
PendingAuthSession
,
email
string
)
map
[
string
]
any
{
response
:=
mergePendingCompletionResponse
(
session
,
map
[
string
]
any
{
"step"
:
oauthPendingChoiceStep
,
"adoption_required"
:
true
,
"force_email_on_signup"
:
true
,
"email_binding_required"
:
true
,
"existing_account_bindable"
:
true
,
})
if
email
=
strings
.
TrimSpace
(
email
);
email
!=
""
{
response
[
"email"
]
=
email
response
[
"resolved_email"
]
=
email
}
return
response
}
func
(
h
*
AuthHandler
)
transitionPendingOAuthAccountToChoiceState
(
c
*
gin
.
Context
,
c
*
gin
.
Context
,
client
*
dbent
.
Client
,
client
*
dbent
.
Client
,
session
*
dbent
.
PendingAuthSession
,
session
*
dbent
.
PendingAuthSession
,
email
string
,
email
string
,
decision
oauthAdoptionDecisionRequest
,
)
(
*
dbent
.
PendingAuthSession
,
error
)
{
)
(
*
dbent
.
PendingAuthSession
,
error
)
{
existingUser
,
err
:=
findUserByNormalizedEmail
(
c
.
Request
.
Context
(),
client
,
email
)
completionResponse
:=
pendingOAuthChoiceCompletionResponse
(
session
,
email
)
if
err
!=
nil
{
session
,
err
:=
updatePendingOAuthSessionProgress
(
return
nil
,
err
}
completionResponse
:=
mergePendingCompletionResponse
(
session
,
map
[
string
]
any
{
"step"
:
"bind_login_required"
,
"email"
:
email
,
})
session
,
err
=
updatePendingOAuthSessionProgress
(
c
.
Request
.
Context
(),
c
.
Request
.
Context
(),
client
,
client
,
session
,
session
,
"adopt_exi
sting
_user_by_email"
,
st
r
ing
s
.
TrimSpace
(
session
.
Intent
)
,
email
,
email
,
&
existingUser
.
ID
,
nil
,
completionResponse
,
completionResponse
,
)
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_SESSION_UPDATE_FAILED"
,
"failed to update pending oauth session"
)
.
WithCause
(
err
)
return
nil
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_SESSION_UPDATE_FAILED"
,
"failed to update pending oauth session"
)
.
WithCause
(
err
)
}
}
if
_
,
err
:=
h
.
ensurePendingOAuthAdoptionDecision
(
c
,
session
.
ID
,
decision
);
err
!=
nil
{
return
nil
,
err
}
return
session
,
nil
return
session
,
nil
}
}
...
@@ -1365,12 +1388,20 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
...
@@ -1365,12 +1388,20 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
email
:=
strings
.
TrimSpace
(
strings
.
ToLower
(
req
.
Email
))
email
:=
strings
.
TrimSpace
(
strings
.
ToLower
(
req
.
Email
))
existingUser
,
err
:=
findUserByNormalizedEmail
(
c
.
Request
.
Context
(),
client
,
email
)
existingUser
,
err
:=
findUserByNormalizedEmail
(
c
.
Request
.
Context
(),
client
,
email
)
if
err
!=
nil
&&
!
errors
.
Is
(
err
,
service
.
ErrUserNotFound
)
{
if
err
!=
nil
{
switch
{
case
errors
.
Is
(
err
,
service
.
ErrUserNotFound
)
:
existingUser
=
nil
case
infraerrors
.
Code
(
err
)
>=
http
.
StatusBadRequest
&&
infraerrors
.
Code
(
err
)
<
http
.
StatusInternalServerError
:
response
.
ErrorFrom
(
c
,
err
)
return
default
:
response
.
ErrorFrom
(
c
,
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
))
response
.
ErrorFrom
(
c
,
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
))
return
return
}
}
}
if
existingUser
!=
nil
{
if
existingUser
!=
nil
{
session
,
err
=
h
.
transitionPendingOAuthAccountTo
BindLogin
(
c
,
client
,
session
,
email
,
req
.
adoptionDecision
()
)
session
,
err
=
h
.
transitionPendingOAuthAccountTo
ChoiceState
(
c
,
client
,
session
,
email
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
@@ -1393,7 +1424,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
...
@@ -1393,7 +1424,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
)
)
if
err
!=
nil
{
if
err
!=
nil
{
if
errors
.
Is
(
err
,
service
.
ErrEmailExists
)
{
if
errors
.
Is
(
err
,
service
.
ErrEmailExists
)
{
session
,
err
=
h
.
transitionPendingOAuthAccountTo
BindLogin
(
c
,
client
,
session
,
email
,
req
.
adoptionDecision
()
)
session
,
err
=
h
.
transitionPendingOAuthAccountTo
ChoiceState
(
c
,
client
,
session
,
email
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
@@ -1548,6 +1579,7 @@ func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
...
@@ -1548,6 +1579,7 @@ func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_COMPLETION_INVALID"
,
"pending auth completion payload is invalid"
))
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_COMPLETION_INVALID"
,
"pending auth completion payload is invalid"
))
return
return
}
}
payload
=
normalizePendingOAuthCompletionResponse
(
payload
)
if
strings
.
TrimSpace
(
session
.
RedirectTo
)
!=
""
{
if
strings
.
TrimSpace
(
session
.
RedirectTo
)
!=
""
{
if
_
,
exists
:=
payload
[
"redirect"
];
!
exists
{
if
_
,
exists
:=
payload
[
"redirect"
];
!
exists
{
payload
[
"redirect"
]
=
session
.
RedirectTo
payload
[
"redirect"
]
=
session
.
RedirectTo
...
...
backend/internal/handler/auth_oidc_oauth.go
View file @
4c21320d
...
@@ -420,27 +420,6 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
...
@@ -420,27 +420,6 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
infraerrors
.
Reason
(
err
),
infraerrors
.
Message
(
err
))
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
infraerrors
.
Reason
(
err
),
infraerrors
.
Message
(
err
))
return
return
}
}
if
compatEmailUser
!=
nil
{
if
err
:=
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
Intent
:
"adopt_existing_user_by_email"
,
Identity
:
identityRef
,
TargetUserID
:
&
compatEmailUser
.
ID
,
ResolvedEmail
:
compatEmailUser
.
Email
,
RedirectTo
:
redirectTo
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
map
[
string
]
any
{
"redirect"
:
redirectTo
,
"step"
:
"bind_login_required"
,
"email"
:
compatEmailUser
.
Email
,
},
});
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
}
redirectToFrontendCallback
(
c
,
frontendCallback
)
return
}
if
cfg
.
RequireEmailVerified
{
if
cfg
.
RequireEmailVerified
{
if
emailVerified
==
nil
||
!*
emailVerified
{
if
emailVerified
==
nil
||
!*
emailVerified
{
...
@@ -450,7 +429,18 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
...
@@ -450,7 +429,18 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
}
}
if
h
.
isForceEmailOnThirdPartySignup
(
c
.
Request
.
Context
())
{
if
h
.
isForceEmailOnThirdPartySignup
(
c
.
Request
.
Context
())
{
if
err
:=
h
.
createOAuthEmailRequiredPendingSession
(
c
,
identityRef
,
redirectTo
,
browserSessionKey
,
upstreamClaims
);
err
!=
nil
{
if
err
:=
h
.
createOIDCOAuthChoicePendingSession
(
c
,
identityRef
,
email
,
email
,
redirectTo
,
browserSessionKey
,
upstreamClaims
,
compatEmail
,
compatEmailUser
,
true
,
);
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
return
}
}
...
@@ -458,48 +448,18 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
...
@@ -458,48 +448,18 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
return
return
}
}
// 传入空邀请码;如果需要邀请码,服务层返回 ErrOAuthInvitationRequired
if
err
:=
h
.
createOIDCOAuthChoicePendingSession
(
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
""
)
c
,
if
err
!=
nil
{
identityRef
,
if
errors
.
Is
(
err
,
service
.
ErrOAuthInvitationRequired
)
{
email
,
if
err
:=
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
email
,
Intent
:
"login"
,
redirectTo
,
Identity
:
identityRef
,
browserSessionKey
,
ResolvedEmail
:
email
,
upstreamClaims
,
RedirectTo
:
redirectTo
,
compatEmail
,
BrowserSessionKey
:
browserSessionKey
,
compatEmailUser
,
UpstreamIdentityClaims
:
upstreamClaims
,
h
.
isForceEmailOnThirdPartySignup
(
c
.
Request
.
Context
()),
CompletionResponse
:
map
[
string
]
any
{
);
err
!=
nil
{
"error"
:
"invitation_required"
,
"redirect"
:
redirectTo
,
},
});
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
}
redirectToFrontendCallback
(
c
,
frontendCallback
)
return
}
redirectOAuthError
(
c
,
frontendCallback
,
"login_failed"
,
infraerrors
.
Reason
(
err
),
infraerrors
.
Message
(
err
))
return
}
if
err
:=
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
Intent
:
"login"
,
Identity
:
identityRef
,
TargetUserID
:
&
user
.
ID
,
ResolvedEmail
:
email
,
RedirectTo
:
redirectTo
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
map
[
string
]
any
{
"access_token"
:
tokenPair
.
AccessToken
,
"refresh_token"
:
tokenPair
.
RefreshToken
,
"expires_in"
:
tokenPair
.
ExpiresIn
,
"token_type"
:
"Bearer"
,
"redirect"
:
redirectTo
,
},
});
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
return
}
}
...
@@ -530,6 +490,65 @@ func (h *AuthHandler) findOIDCCompatEmailUser(ctx context.Context, email string)
...
@@ -530,6 +490,65 @@ func (h *AuthHandler) findOIDCCompatEmailUser(ctx context.Context, email string)
return
userEntity
,
nil
return
userEntity
,
nil
}
}
func
(
h
*
AuthHandler
)
createOIDCOAuthChoicePendingSession
(
c
*
gin
.
Context
,
identity
service
.
PendingAuthIdentityKey
,
suggestedEmail
string
,
resolvedEmail
string
,
redirectTo
string
,
browserSessionKey
string
,
upstreamClaims
map
[
string
]
any
,
compatEmail
string
,
compatEmailUser
*
dbent
.
User
,
forceEmailOnSignup
bool
,
)
error
{
suggestionEmail
:=
strings
.
TrimSpace
(
suggestedEmail
)
canonicalEmail
:=
strings
.
TrimSpace
(
resolvedEmail
)
if
suggestionEmail
==
""
{
suggestionEmail
=
canonicalEmail
}
completionResponse
:=
map
[
string
]
any
{
"step"
:
oauthPendingChoiceStep
,
"adoption_required"
:
true
,
"redirect"
:
strings
.
TrimSpace
(
redirectTo
),
"email"
:
suggestionEmail
,
"resolved_email"
:
canonicalEmail
,
"existing_account_email"
:
""
,
"existing_account_bindable"
:
false
,
"create_account_allowed"
:
true
,
"force_email_on_signup"
:
forceEmailOnSignup
,
"choice_reason"
:
"third_party_signup"
,
}
if
strings
.
TrimSpace
(
compatEmail
)
!=
""
{
completionResponse
[
"compat_email"
]
=
strings
.
TrimSpace
(
compatEmail
)
}
if
compatEmailUser
!=
nil
{
completionResponse
[
"email"
]
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
completionResponse
[
"existing_account_email"
]
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
completionResponse
[
"existing_account_bindable"
]
=
true
completionResponse
[
"choice_reason"
]
=
"compat_email_match"
}
if
forceEmailOnSignup
&&
compatEmailUser
==
nil
{
completionResponse
[
"choice_reason"
]
=
"force_email_on_signup"
}
resolvedChoiceEmail
:=
suggestionEmail
if
compatEmailUser
!=
nil
{
resolvedChoiceEmail
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
}
return
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
Intent
:
oauthIntentLogin
,
Identity
:
identity
,
ResolvedEmail
:
resolvedChoiceEmail
,
RedirectTo
:
redirectTo
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
completionResponse
,
})
}
type
completeOIDCOAuthRequest
struct
{
type
completeOIDCOAuthRequest
struct
{
InvitationCode
string
`json:"invitation_code" binding:"required"`
InvitationCode
string
`json:"invitation_code" binding:"required"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
...
...
backend/internal/handler/auth_wechat_oauth.go
View file @
4c21320d
...
@@ -62,6 +62,8 @@ type wechatOAuthConfig struct {
...
@@ -62,6 +62,8 @@ type wechatOAuthConfig struct {
scope
string
scope
string
redirectURI
string
redirectURI
string
frontendCallback
string
frontendCallback
string
openEnabled
bool
mpEnabled
bool
}
}
type
wechatOAuthTokenResponse
struct
{
type
wechatOAuthTokenResponse
struct
{
...
@@ -209,11 +211,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
...
@@ -209,11 +211,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
unionid
:=
strings
.
TrimSpace
(
firstNonEmpty
(
userInfo
.
UnionID
,
tokenResp
.
UnionID
))
unionid
:=
strings
.
TrimSpace
(
firstNonEmpty
(
userInfo
.
UnionID
,
tokenResp
.
UnionID
))
openid
:=
strings
.
TrimSpace
(
firstNonEmpty
(
userInfo
.
OpenID
,
tokenResp
.
OpenID
))
openid
:=
strings
.
TrimSpace
(
firstNonEmpty
(
userInfo
.
OpenID
,
tokenResp
.
OpenID
))
if
unionid
==
""
{
providerSubject
:=
unionid
if
providerSubject
==
""
{
if
cfg
.
requiresUnionID
()
{
redirectOAuthError
(
c
,
frontendCallback
,
"provider_error"
,
"wechat_missing_unionid"
,
""
)
return
}
providerSubject
=
openid
}
if
providerSubject
==
""
{
redirectOAuthError
(
c
,
frontendCallback
,
"provider_error"
,
"wechat_missing_unionid"
,
""
)
redirectOAuthError
(
c
,
frontendCallback
,
"provider_error"
,
"wechat_missing_unionid"
,
""
)
return
return
}
}
providerSubject
:=
unionid
username
:=
firstNonEmpty
(
userInfo
.
Nickname
,
wechatFallbackUsername
(
providerSubject
))
username
:=
firstNonEmpty
(
userInfo
.
Nickname
,
wechatFallbackUsername
(
providerSubject
))
email
:=
wechatSyntheticEmail
(
providerSubject
)
email
:=
wechatSyntheticEmail
(
providerSubject
)
...
@@ -284,17 +293,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
...
@@ -284,17 +293,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
}
}
if
h
.
isForceEmailOnThirdPartySignup
(
c
.
Request
.
Context
())
{
if
h
.
isForceEmailOnThirdPartySignup
(
c
.
Request
.
Context
())
{
if
err
:=
h
.
createOAuthEmailRequiredPendingSession
(
c
,
identityRef
,
redirectTo
,
browserSessionKey
,
upstreamClaims
);
err
!=
nil
{
if
err
:=
h
.
createWeChatChoicePendingSession
(
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
c
,
return
identityRef
,
}
email
,
redirectToFrontendCallback
(
c
,
frontendCallback
)
email
,
return
redirectTo
,
}
browserSessionKey
,
upstreamClaims
,
tokenPair
,
_
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
""
)
""
,
if
err
!=
nil
{
nil
,
if
err
:=
h
.
createWeChatPendingSession
(
c
,
normalizedIntent
,
providerSubject
,
email
,
redirectTo
,
browserSessionKey
,
upstreamClaims
,
tokenPair
,
err
,
nil
);
err
!=
nil
{
true
,
);
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
return
}
}
...
@@ -302,7 +312,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
...
@@ -302,7 +312,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
return
return
}
}
if
err
:=
h
.
createWeChatPendingSession
(
c
,
normalizedIntent
,
providerSubject
,
email
,
redirectTo
,
browserSessionKey
,
upstreamClaims
,
tokenPair
,
nil
,
nil
);
err
!=
nil
{
if
err
:=
h
.
createWeChatChoicePendingSession
(
c
,
identityRef
,
email
,
email
,
redirectTo
,
browserSessionKey
,
upstreamClaims
,
""
,
nil
,
false
,
);
err
!=
nil
{
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
redirectOAuthError
(
c
,
frontendCallback
,
"session_error"
,
"failed to continue oauth login"
,
""
)
return
return
}
}
...
@@ -600,6 +621,65 @@ func (h *AuthHandler) createWeChatPendingSession(
...
@@ -600,6 +621,65 @@ func (h *AuthHandler) createWeChatPendingSession(
})
})
}
}
func
(
h
*
AuthHandler
)
createWeChatChoicePendingSession
(
c
*
gin
.
Context
,
identity
service
.
PendingAuthIdentityKey
,
suggestedEmail
string
,
resolvedEmail
string
,
redirectTo
string
,
browserSessionKey
string
,
upstreamClaims
map
[
string
]
any
,
compatEmail
string
,
compatEmailUser
*
dbent
.
User
,
forceEmailOnSignup
bool
,
)
error
{
suggestionEmail
:=
strings
.
TrimSpace
(
suggestedEmail
)
canonicalEmail
:=
strings
.
TrimSpace
(
resolvedEmail
)
if
suggestionEmail
==
""
{
suggestionEmail
=
canonicalEmail
}
completionResponse
:=
map
[
string
]
any
{
"step"
:
oauthPendingChoiceStep
,
"adoption_required"
:
true
,
"redirect"
:
strings
.
TrimSpace
(
redirectTo
),
"email"
:
suggestionEmail
,
"resolved_email"
:
canonicalEmail
,
"existing_account_email"
:
""
,
"existing_account_bindable"
:
false
,
"create_account_allowed"
:
true
,
"force_email_on_signup"
:
forceEmailOnSignup
,
"choice_reason"
:
"third_party_signup"
,
}
if
strings
.
TrimSpace
(
compatEmail
)
!=
""
{
completionResponse
[
"compat_email"
]
=
strings
.
TrimSpace
(
compatEmail
)
}
if
compatEmailUser
!=
nil
{
completionResponse
[
"email"
]
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
completionResponse
[
"existing_account_email"
]
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
completionResponse
[
"existing_account_bindable"
]
=
true
completionResponse
[
"choice_reason"
]
=
"compat_email_match"
}
if
forceEmailOnSignup
{
completionResponse
[
"choice_reason"
]
=
"force_email_on_signup"
}
resolvedChoiceEmail
:=
suggestionEmail
if
compatEmailUser
!=
nil
{
resolvedChoiceEmail
=
strings
.
TrimSpace
(
compatEmailUser
.
Email
)
}
return
h
.
createOAuthPendingSession
(
c
,
oauthPendingSessionPayload
{
Intent
:
oauthIntentLogin
,
Identity
:
identity
,
ResolvedEmail
:
resolvedChoiceEmail
,
RedirectTo
:
redirectTo
,
BrowserSessionKey
:
browserSessionKey
,
UpstreamIdentityClaims
:
upstreamClaims
,
CompletionResponse
:
completionResponse
,
})
}
func
(
h
*
AuthHandler
)
createWeChatBindPendingSession
(
func
(
h
*
AuthHandler
)
createWeChatBindPendingSession
(
c
*
gin
.
Context
,
c
*
gin
.
Context
,
cfg
wechatOAuthConfig
,
cfg
wechatOAuthConfig
,
...
@@ -874,7 +954,7 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
...
@@ -874,7 +954,7 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
if
err
!=
nil
{
if
err
!=
nil
{
return
wechatOAuthConfig
{},
err
return
wechatOAuthConfig
{},
err
}
}
if
effective
.
Mode
!=
mode
{
if
!
effective
.
SupportsMode
(
mode
)
{
return
wechatOAuthConfig
{},
infraerrors
.
NotFound
(
"OAUTH_DISABLED"
,
"wechat oauth is disabled"
)
return
wechatOAuthConfig
{},
infraerrors
.
NotFound
(
"OAUTH_DISABLED"
,
"wechat oauth is disabled"
)
}
}
...
@@ -884,7 +964,9 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
...
@@ -884,7 +964,9 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
appSecret
:
strings
.
TrimSpace
(
effective
.
AppSecret
),
appSecret
:
strings
.
TrimSpace
(
effective
.
AppSecret
),
redirectURI
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
RedirectURL
),
resolveWeChatOAuthAbsoluteURL
(
apiBaseURL
,
c
,
"/api/v1/auth/oauth/wechat/callback"
)),
redirectURI
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
RedirectURL
),
resolveWeChatOAuthAbsoluteURL
(
apiBaseURL
,
c
,
"/api/v1/auth/oauth/wechat/callback"
)),
frontendCallback
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
FrontendRedirectURL
),
wechatOAuthDefaultFrontendCB
),
frontendCallback
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
FrontendRedirectURL
),
wechatOAuthDefaultFrontendCB
),
scope
:
firstNonEmpty
(
strings
.
TrimSpace
(
effective
.
Scopes
),
service
.
DefaultWeChatConnectScopesForMode
(
mode
)),
scope
:
effective
.
ScopeForMode
(
mode
),
openEnabled
:
effective
.
OpenEnabled
,
mpEnabled
:
effective
.
MPEnabled
,
}
}
switch
mode
{
switch
mode
{
...
@@ -900,6 +982,10 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
...
@@ -900,6 +982,10 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
return
cfg
,
nil
return
cfg
,
nil
}
}
func
(
cfg
wechatOAuthConfig
)
requiresUnionID
()
bool
{
return
cfg
.
openEnabled
&&
cfg
.
mpEnabled
}
func
(
h
*
AuthHandler
)
wechatOAuthFrontendCallback
(
ctx
context
.
Context
)
string
{
func
(
h
*
AuthHandler
)
wechatOAuthFrontendCallback
(
ctx
context
.
Context
)
string
{
if
h
!=
nil
&&
h
.
settingSvc
!=
nil
{
if
h
!=
nil
&&
h
.
settingSvc
!=
nil
{
cfg
,
err
:=
h
.
settingSvc
.
GetWeChatConnectOAuthConfig
(
ctx
)
cfg
,
err
:=
h
.
settingSvc
.
GetWeChatConnectOAuthConfig
(
ctx
)
...
...
backend/internal/handler/auth_wechat_oauth_test.go
View file @
4c21320d
...
@@ -65,6 +65,36 @@ func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
...
@@ -65,6 +65,36 @@ func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
require
.
NotEmpty
(
t
,
findCookie
(
cookies
,
oauthPendingBrowserCookieName
))
require
.
NotEmpty
(
t
,
findCookie
(
cookies
,
oauthPendingBrowserCookieName
))
}
}
func
TestWeChatOAuthStart_AllowsOpenModeWhenBothCapabilitiesEnabled
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
handler
,
client
:=
newWeChatOAuthTestHandlerWithSettings
(
t
,
false
,
map
[
string
]
string
{
service
.
SettingKeyWeChatConnectEnabled
:
"true"
,
service
.
SettingKeyWeChatConnectAppID
:
"wx-shared-app"
,
service
.
SettingKeyWeChatConnectAppSecret
:
"wx-shared-secret"
,
service
.
SettingKeyWeChatConnectMode
:
"mp"
,
service
.
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
service
.
SettingKeyWeChatConnectOpenEnabled
:
"true"
,
service
.
SettingKeyWeChatConnectMPEnabled
:
"true"
,
service
.
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
service
.
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
})
defer
client
.
Close
()
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing"
,
nil
)
c
.
Request
.
Host
=
"api.example.com"
handler
.
WeChatOAuthStart
(
c
)
require
.
Equal
(
t
,
http
.
StatusFound
,
recorder
.
Code
)
location
:=
recorder
.
Header
()
.
Get
(
"Location"
)
require
.
NotEmpty
(
t
,
location
)
require
.
Contains
(
t
,
location
,
"open.weixin.qq.com"
)
require
.
Contains
(
t
,
location
,
"connect/qrconnect"
)
require
.
Contains
(
t
,
location
,
"scope=snsapi_login"
)
}
func
TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow
(
t
*
testing
.
T
)
{
func
TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow
(
t
*
testing
.
T
)
{
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
originalUserInfoURL
:=
wechatOAuthUserInfoURL
...
...
frontend/src/views/auth/LinuxDoCallbackView.vue
View file @
4c21320d
...
@@ -15,6 +15,7 @@
...
@@ -15,6 +15,7 @@
v-if=
"
v-if=
"
needsInvitation ||
needsInvitation ||
needsAdoptionConfirmation ||
needsAdoptionConfirmation ||
needsChooser ||
needsCreateAccount ||
needsCreateAccount ||
needsBindLogin ||
needsBindLogin ||
needsTotpChallenge
needsTotpChallenge
...
@@ -109,6 +110,42 @@
...
@@ -109,6 +110,42 @@
</button>
</button>
</
template
>
</
template
>
<
template
v-else-if=
"needsChooser"
>
<div
class=
"rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-800/60"
>
<div
class=
"space-y-4"
>
<div
class=
"space-y-1"
>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
Choose how to continue
</p>
<p
class=
"text-xs text-gray-500 dark:text-dark-400"
>
{{
pendingAccountEmail
?
`Suggested email: ${pendingAccountEmail
}
`
:
'
Choose whether to bind an existing account or create a new one.
'
}}
<
/p
>
<
/div
>
<
div
class
=
"
grid gap-3 sm:grid-cols-2
"
>
<
button
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToBindLoginMode()
"
>
Bind
existing
account
<
/button
>
<
button
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToCreateAccountMode
"
>
Create
new
account
<
/button
>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsCreateAccount
"
>
<
template
v
-
else
-
if
=
"
needsCreateAccount
"
>
<
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
.
...
@@ -275,7 +312,7 @@ const suggestedAvatarUrl = ref('')
...
@@ -275,7 +312,7 @@ const suggestedAvatarUrl = ref('')
const
adoptDisplayName
=
ref
(
true
)
const
adoptDisplayName
=
ref
(
true
)
const
adoptAvatar
=
ref
(
true
)
const
adoptAvatar
=
ref
(
true
)
const
needsAdoptionConfirmation
=
ref
(
false
)
const
needsAdoptionConfirmation
=
ref
(
false
)
const
pendingAccountAction
=
ref
<
'
none
'
|
'
create_account
'
|
'
bind_login
'
>
(
'
none
'
)
const
pendingAccountAction
=
ref
<
'
none
'
|
'
choose_account_action
'
|
'
create_account
'
|
'
bind_login
'
>
(
'
none
'
)
const
pendingAccountEmail
=
ref
(
''
)
const
pendingAccountEmail
=
ref
(
''
)
const
bindLoginEmail
=
ref
(
''
)
const
bindLoginEmail
=
ref
(
''
)
const
bindLoginPassword
=
ref
(
''
)
const
bindLoginPassword
=
ref
(
''
)
...
@@ -290,12 +327,17 @@ const totpError = ref('')
...
@@ -290,12 +327,17 @@ const totpError = ref('')
const
totpUserEmailMasked
=
ref
(
''
)
const
totpUserEmailMasked
=
ref
(
''
)
const
needsCreateAccount
=
computed
(()
=>
pendingAccountAction
.
value
===
'
create_account
'
)
const
needsCreateAccount
=
computed
(()
=>
pendingAccountAction
.
value
===
'
create_account
'
)
const
needsChooser
=
computed
(()
=>
pendingAccountAction
.
value
===
'
choose_account_action
'
)
const
needsBindLogin
=
computed
(()
=>
pendingAccountAction
.
value
===
'
bind_login
'
)
const
needsBindLogin
=
computed
(()
=>
pendingAccountAction
.
value
===
'
bind_login
'
)
type
LinuxDoPendingActionResponse
=
PendingOAuthExchangeResponse
&
{
type
LinuxDoPendingActionResponse
=
PendingOAuthExchangeResponse
&
{
step
?:
string
step
?:
string
intent
?:
string
email
?:
string
email
?:
string
resolved_email
?:
string
resolved_email
?:
string
pending_email
?:
string
existing_account_email
?:
string
suggested_email
?:
string
}
}
function
persistPendingAuthSession
(
redirect
?:
string
)
{
function
persistPendingAuthSession
(
redirect
?:
string
)
{
...
@@ -392,12 +434,34 @@ function hasSuggestedProfile(completion: {
...
@@ -392,12 +434,34 @@ function hasSuggestedProfile(completion: {
return
Boolean
(
completion
.
suggested_display_name
||
completion
.
suggested_avatar_url
)
return
Boolean
(
completion
.
suggested_display_name
||
completion
.
suggested_avatar_url
)
}
}
function
normalizedPendingState
(
value
:
string
|
null
|
undefined
):
string
{
return
value
?.
trim
().
toLowerCase
()
||
''
}
function
extractPendingAccountEmail
(
completion
:
LinuxDoPendingActionResponse
):
string
{
function
extractPendingAccountEmail
(
completion
:
LinuxDoPendingActionResponse
):
string
{
return
(
completion
.
email
||
completion
.
resolved_email
||
''
).
trim
()
return
(
completion
.
pending_email
||
completion
.
existing_account_email
||
completion
.
email
||
completion
.
resolved_email
||
completion
.
suggested_email
||
''
).
trim
()
}
}
function
resolvePendingAccountAction
(
completion
:
LinuxDoPendingActionResponse
):
'
none
'
|
'
create_account
'
|
'
bind_login
'
{
function
resolvePendingAccountAction
(
const
raw
=
(
completion
.
step
||
completion
.
error
||
''
).
trim
().
toLowerCase
()
completion
:
LinuxDoPendingActionResponse
):
'
none
'
|
'
choose_account_action
'
|
'
create_account
'
|
'
bind_login
'
{
const
raw
=
normalizedPendingState
(
completion
.
step
||
completion
.
error
||
completion
.
intent
)
if
(
raw
===
'
choice
'
||
raw
===
'
choose_account_action_required
'
||
raw
===
'
choose_account_action
'
||
raw
===
'
choose_account
'
||
raw
===
'
choose
'
)
{
return
'
choose_account_action
'
}
if
(
raw
===
'
email_required
'
||
raw
===
'
create_account_required
'
||
raw
===
'
create_account
'
)
{
if
(
raw
===
'
email_required
'
||
raw
===
'
create_account_required
'
||
raw
===
'
create_account
'
)
{
return
'
create_account
'
return
'
create_account
'
}
}
...
@@ -418,6 +482,14 @@ function applyPendingAccountAction(completion: LinuxDoPendingActionResponse) {
...
@@ -418,6 +482,14 @@ function applyPendingAccountAction(completion: LinuxDoPendingActionResponse) {
totpUserEmailMasked
.
value
=
''
totpUserEmailMasked
.
value
=
''
const
email
=
extractPendingAccountEmail
(
completion
)
const
email
=
extractPendingAccountEmail
(
completion
)
if
(
action
===
'
choose_account_action
'
)
{
pendingAccountEmail
.
value
=
email
bindLoginEmail
.
value
=
email
bindLoginPassword
.
value
=
''
canReturnToCreateAccount
.
value
=
false
return
}
if
(
action
===
'
create_account
'
)
{
if
(
action
===
'
create_account
'
)
{
pendingAccountEmail
.
value
=
email
pendingAccountEmail
.
value
=
email
canReturnToCreateAccount
.
value
=
true
canReturnToCreateAccount
.
value
=
true
...
@@ -470,28 +542,6 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -470,28 +542,6 @@ 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
isCreateAccountRecoveryError
(
error
:
unknown
):
boolean
{
const
data
=
(
error
as
{
response
?:
{
data
?:
{
reason
?:
string
error
?:
string
code
?:
string
step
?:
string
intent
?:
string
}
}
}).
response
?.
data
const
states
=
[
data
?.
reason
,
data
?.
error
,
data
?.
code
,
data
?.
step
,
data
?.
intent
]
.
map
(
value
=>
value
?.
trim
().
toLowerCase
())
.
filter
((
value
):
value
is
string
=>
Boolean
(
value
))
return
states
.
includes
(
'
email_exists
'
)
||
states
.
includes
(
'
bind_login_required
'
)
||
states
.
includes
(
'
bind_login
'
)
||
states
.
includes
(
'
adopt_existing_user_by_email
'
)
}
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
'
)
...
@@ -601,10 +651,6 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
...
@@ -601,10 +651,6 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
}
)
}
)
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
if
(
isCreateAccountRecoveryError
(
e
))
{
switchToBindLoginMode
(
payload
.
email
)
return
}
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
}
finally
{
}
finally
{
isSubmitting
.
value
=
false
isSubmitting
.
value
=
false
...
...
frontend/src/views/auth/OidcCallbackView.vue
View file @
4c21320d
...
@@ -19,6 +19,7 @@
...
@@ -19,6 +19,7 @@
v
-
if
=
"
v
-
if
=
"
needsInvitation ||
needsInvitation ||
needsAdoptionConfirmation ||
needsAdoptionConfirmation ||
needsChooser ||
needsCreateAccount ||
needsCreateAccount ||
needsBindLogin ||
needsBindLogin ||
needsTotpChallenge
needsTotpChallenge
...
@@ -118,6 +119,42 @@
...
@@ -118,6 +119,42 @@
<
/button
>
<
/button
>
<
/template
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsChooser
"
>
<
div
class
=
"
rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-800/60
"
>
<
div
class
=
"
space-y-4
"
>
<
div
class
=
"
space-y-1
"
>
<
p
class
=
"
text-sm font-medium text-gray-900 dark:text-white
"
>
Choose
how
to
continue
<
/p
>
<
p
class
=
"
text-xs text-gray-500 dark:text-dark-400
"
>
{{
pendingAccountEmail
?
`Suggested email: ${pendingAccountEmail
}
`
:
`Choose whether to bind an existing ${providerName
}
account or create a new one.`
}}
<
/p
>
<
/div
>
<
div
class
=
"
grid gap-3 sm:grid-cols-2
"
>
<
button
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToBindLoginMode()
"
>
Bind
existing
account
<
/button
>
<
button
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToCreateAccountMode
"
>
Create
new
account
<
/button
>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsCreateAccount
"
>
<
template
v
-
else
-
if
=
"
needsCreateAccount
"
>
<
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
.
...
@@ -284,7 +321,7 @@ const suggestedAvatarUrl = ref('')
...
@@ -284,7 +321,7 @@ const suggestedAvatarUrl = ref('')
const
adoptDisplayName
=
ref
(
true
)
const
adoptDisplayName
=
ref
(
true
)
const
adoptAvatar
=
ref
(
true
)
const
adoptAvatar
=
ref
(
true
)
const
needsAdoptionConfirmation
=
ref
(
false
)
const
needsAdoptionConfirmation
=
ref
(
false
)
const
pendingAccountAction
=
ref
<
'
none
'
|
'
create_account
'
|
'
bind_login
'
>
(
'
none
'
)
const
pendingAccountAction
=
ref
<
'
none
'
|
'
choose_account_action
'
|
'
create_account
'
|
'
bind_login
'
>
(
'
none
'
)
const
pendingAccountEmail
=
ref
(
''
)
const
pendingAccountEmail
=
ref
(
''
)
const
bindLoginEmail
=
ref
(
''
)
const
bindLoginEmail
=
ref
(
''
)
const
bindLoginPassword
=
ref
(
''
)
const
bindLoginPassword
=
ref
(
''
)
...
@@ -299,6 +336,7 @@ const totpError = ref('')
...
@@ -299,6 +336,7 @@ const totpError = ref('')
const
totpUserEmailMasked
=
ref
(
''
)
const
totpUserEmailMasked
=
ref
(
''
)
const
needsCreateAccount
=
computed
(()
=>
pendingAccountAction
.
value
===
'
create_account
'
)
const
needsCreateAccount
=
computed
(()
=>
pendingAccountAction
.
value
===
'
create_account
'
)
const
needsChooser
=
computed
(()
=>
pendingAccountAction
.
value
===
'
choose_account_action
'
)
const
needsBindLogin
=
computed
(()
=>
pendingAccountAction
.
value
===
'
bind_login
'
)
const
needsBindLogin
=
computed
(()
=>
pendingAccountAction
.
value
===
'
bind_login
'
)
type
PendingOidcCompletion
=
PendingOAuthExchangeResponse
&
{
type
PendingOidcCompletion
=
PendingOAuthExchangeResponse
&
{
...
@@ -307,6 +345,7 @@ type PendingOidcCompletion = PendingOAuthExchangeResponse & {
...
@@ -307,6 +345,7 @@ type PendingOidcCompletion = PendingOAuthExchangeResponse & {
resolved_email
?:
string
resolved_email
?:
string
existing_account_email
?:
string
existing_account_email
?:
string
email
?:
string
email
?:
string
suggested_email
?:
string
provider_fallback
?:
string
provider_fallback
?:
string
intent
?:
string
intent
?:
string
requires_2fa
?:
boolean
requires_2fa
?:
boolean
...
@@ -430,12 +469,24 @@ function extractPendingAccountEmail(completion: PendingOidcCompletion): string {
...
@@ -430,12 +469,24 @@ function extractPendingAccountEmail(completion: PendingOidcCompletion): string {
completion
.
existing_account_email
||
completion
.
existing_account_email
||
completion
.
resolved_email
||
completion
.
resolved_email
||
completion
.
email
||
completion
.
email
||
completion
.
suggested_email
||
''
''
).
trim
()
).
trim
()
}
}
function
resolvePendingAccountAction
(
completion
:
PendingOidcCompletion
):
'
none
'
|
'
create_account
'
|
'
bind_login
'
{
function
resolvePendingAccountAction
(
completion
:
PendingOidcCompletion
):
'
none
'
|
'
choose_account_action
'
|
'
create_account
'
|
'
bind_login
'
{
const
raw
=
normalizedPendingState
(
completion
.
step
||
completion
.
error
||
completion
.
intent
)
const
raw
=
normalizedPendingState
(
completion
.
step
||
completion
.
error
||
completion
.
intent
)
if
(
raw
===
'
choice
'
||
raw
===
'
choose_account_action_required
'
||
raw
===
'
choose_account_action
'
||
raw
===
'
choose_account
'
||
raw
===
'
choose
'
)
{
return
'
choose_account_action
'
}
if
(
raw
===
'
email_required
'
||
raw
===
'
create_account_required
'
||
raw
===
'
create_account
'
)
{
if
(
raw
===
'
email_required
'
||
raw
===
'
create_account_required
'
||
raw
===
'
create_account
'
)
{
return
'
create_account
'
return
'
create_account
'
}
}
...
@@ -462,6 +513,14 @@ function applyPendingAccountAction(completion: PendingOidcCompletion) {
...
@@ -462,6 +513,14 @@ function applyPendingAccountAction(completion: PendingOidcCompletion) {
totpUserEmailMasked
.
value
=
''
totpUserEmailMasked
.
value
=
''
const
email
=
extractPendingAccountEmail
(
completion
)
const
email
=
extractPendingAccountEmail
(
completion
)
if
(
action
===
'
choose_account_action
'
)
{
pendingAccountEmail
.
value
=
email
bindLoginEmail
.
value
=
email
bindLoginPassword
.
value
=
''
canReturnToCreateAccount
.
value
=
false
return
}
if
(
action
===
'
create_account
'
)
{
if
(
action
===
'
create_account
'
)
{
pendingAccountEmail
.
value
=
email
pendingAccountEmail
.
value
=
email
canReturnToCreateAccount
.
value
=
true
canReturnToCreateAccount
.
value
=
true
...
@@ -514,28 +573,6 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -514,28 +573,6 @@ 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
isCreateAccountRecoveryError
(
error
:
unknown
):
boolean
{
const
data
=
(
error
as
{
response
?:
{
data
?:
{
reason
?:
string
error
?:
string
code
?:
string
step
?:
string
intent
?:
string
}
}
}
).
response
?.
data
const
states
=
[
data
?.
reason
,
data
?.
error
,
data
?.
code
,
data
?.
step
,
data
?.
intent
]
.
map
(
value
=>
value
?.
trim
().
toLowerCase
())
.
filter
((
value
):
value
is
string
=>
Boolean
(
value
))
return
states
.
includes
(
'
email_exists
'
)
||
states
.
includes
(
'
bind_login_required
'
)
||
states
.
includes
(
'
bind_login
'
)
||
states
.
includes
(
'
adopt_existing_user_by_email
'
)
}
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
'
)
...
@@ -645,10 +682,6 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
...
@@ -645,10 +682,6 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
}
)
}
)
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
if
(
isCreateAccountRecoveryError
(
e
))
{
switchToBindLoginMode
(
payload
.
email
)
return
}
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
}
finally
{
}
finally
{
isSubmitting
.
value
=
false
isSubmitting
.
value
=
false
...
...
frontend/src/views/auth/WechatCallbackView.vue
View file @
4c21320d
...
@@ -18,6 +18,7 @@
...
@@ -18,6 +18,7 @@
<
div
<
div
v
-
if
=
"
v
-
if
=
"
needsInvitation ||
needsInvitation ||
needsChooser ||
needsAdoptionConfirmation ||
needsAdoptionConfirmation ||
needsCreateAccount ||
needsCreateAccount ||
needsBindLogin ||
needsBindLogin ||
...
@@ -147,6 +148,43 @@
...
@@ -147,6 +148,43 @@
<
/div
>
<
/div
>
<
/template
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsChooser
"
>
<
div
class
=
"
rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-800/60
"
>
<
div
class
=
"
space-y-4
"
>
<
div
class
=
"
space-y-1
"
>
<
p
class
=
"
text-sm font-medium text-gray-900 dark:text-white
"
>
Choose
how
to
continue
<
/p
>
<
p
class
=
"
text-xs text-gray-500 dark:text-dark-400
"
>
Pick
whether
to
bind
an
existing
account
or
create
a
new
one
.
<
/p
>
<
/div
>
<
button
data
-
testid
=
"
wechat-choice-bind-existing
"
type
=
"
button
"
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToBindLoginMode()
"
>
Bind
existing
account
<
/button
>
<
button
data
-
testid
=
"
wechat-choice-create-account
"
type
=
"
button
"
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToCreateAccountMode()
"
>
Create
new
account
<
/button
>
<
/div
>
<
/div
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsAdoptionConfirmation
"
>
<
template
v
-
else
-
if
=
"
needsAdoptionConfirmation
"
>
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
Review
the
{{
providerName
}}
profile
details
before
continuing
.
Review
the
{{
providerName
}}
profile
details
before
continuing
.
...
@@ -168,13 +206,46 @@
...
@@ -168,13 +206,46 @@
@
submit
=
"
handleCreateAccount
"
@
submit
=
"
handleCreateAccount
"
@
switch
-
to
-
bind
=
"
switchToBindLoginMode
"
@
switch
-
to
-
bind
=
"
switchToBindLoginMode
"
/>
/>
<
button
v
-
if
=
"
showBackToChooser
"
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToChoiceMode
"
>
Back
to
options
<
/button
>
<
/template
>
<
/template
>
<
template
v
-
else
-
if
=
"
needsBindLogin
"
>
<
template
v
-
else
-
if
=
"
needsBindLogin
"
>
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
<
p
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
Log
in
to
an
existing
account
to
b
ind
this
{{
providerName
}}
sign
-
in
.
B
ind
this
{{
providerName
}}
sign
-
in
to
an
existing
account
.
<
/p
>
<
/p
>
<
div
v
-
if
=
"
hasCurrentAuthToken
"
class
=
"
rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-800/60
"
>
<
div
class
=
"
space-y-3
"
>
<
div
class
=
"
space-y-3
"
>
<
div
class
=
"
space-y-1
"
>
<
p
class
=
"
text-sm font-medium text-gray-900 dark:text-white
"
>
Bind
the
current
account
<
/p
>
<
p
class
=
"
text-xs text-gray-500 dark:text-dark-400
"
>
Bind
this
WeChat
identity
to
the
account
currently
signed
in
on
this
browser
.
<
/p
>
<
/div
>
<
button
data
-
testid
=
"
existing-account-submit
"
type
=
"
button
"
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
handleBindCurrentAccount
"
>
{{
isSubmitting
?
t
(
'
common.processing
'
)
:
'
Bind current account
'
}}
<
/button
>
<
/div
>
<
/div
>
<
div
v
-
else
class
=
"
space-y-3
"
>
<
input
<
input
v
-
model
=
"
bindLoginEmail
"
v
-
model
=
"
bindLoginEmail
"
data
-
testid
=
"
wechat-bind-login-email
"
data
-
testid
=
"
wechat-bind-login-email
"
...
@@ -201,20 +272,15 @@
...
@@ -201,20 +272,15 @@
>
>
{{
isSubmitting
?
t
(
'
common.processing
'
)
:
'
Log in and bind
'
}}
{{
isSubmitting
?
t
(
'
common.processing
'
)
:
'
Log in and bind
'
}}
<
/button
>
<
/button
>
<
/div
>
<
button
<
button
v
-
if
=
"
canReturnToCreateAccount
"
v
-
if
=
"
showBackToChooser
"
class
=
"
btn btn-secondary w-full
"
class
=
"
btn btn-secondary w-full
"
:
disabled
=
"
isSubmitting
"
:
disabled
=
"
isSubmitting
"
@
click
=
"
switchToC
reateAccount
Mode
"
@
click
=
"
switchToC
hoice
Mode
"
>
>
Use
a
different
email
Back
to
options
<
/button
>
<
/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
=
"
needsTotpChallenge
"
>
<
template
v
-
else
-
if
=
"
needsTotpChallenge
"
>
...
@@ -253,6 +319,12 @@
...
@@ -253,6 +319,12 @@
<
/div
>
<
/div
>
<
/transition
>
<
/transition
>
<
transition
name
=
"
fade
"
>
<
p
v
-
if
=
"
accountActionError
"
class
=
"
text-sm text-red-600 dark:text-red-400
"
>
{{
accountActionError
}}
<
/p
>
<
/transition
>
<
transition
name
=
"
fade
"
>
<
transition
name
=
"
fade
"
>
<
div
<
div
v
-
if
=
"
errorMessage
"
v
-
if
=
"
errorMessage
"
...
@@ -314,6 +386,7 @@ const appStore = useAppStore()
...
@@ -314,6 +386,7 @@ const appStore = useAppStore()
const
isProcessing
=
ref
(
true
)
const
isProcessing
=
ref
(
true
)
const
errorMessage
=
ref
(
''
)
const
errorMessage
=
ref
(
''
)
const
needsInvitation
=
ref
(
false
)
const
needsInvitation
=
ref
(
false
)
const
needsChooser
=
ref
(
false
)
const
invitationCode
=
ref
(
''
)
const
invitationCode
=
ref
(
''
)
const
isSubmitting
=
ref
(
false
)
const
isSubmitting
=
ref
(
false
)
const
invitationError
=
ref
(
''
)
const
invitationError
=
ref
(
''
)
...
@@ -325,13 +398,12 @@ const existingAccountEmail = ref('')
...
@@ -325,13 +398,12 @@ const existingAccountEmail = ref('')
const
adoptDisplayName
=
ref
(
true
)
const
adoptDisplayName
=
ref
(
true
)
const
adoptAvatar
=
ref
(
true
)
const
adoptAvatar
=
ref
(
true
)
const
needsAdoptionConfirmation
=
ref
(
false
)
const
needsAdoptionConfirmation
=
ref
(
false
)
const
pendingAccountAction
=
ref
<
'
none
'
|
'
create_account
'
|
'
bind_login
'
>
(
'
none
'
)
const
pendingAccountAction
=
ref
<
'
none
'
|
'
choice
'
|
'
create_account
'
|
'
bind_login
'
>
(
'
none
'
)
const
pendingAccountEmail
=
ref
(
''
)
const
pendingAccountEmail
=
ref
(
''
)
const
bindLoginEmail
=
ref
(
''
)
const
bindLoginEmail
=
ref
(
''
)
const
bindLoginPassword
=
ref
(
''
)
const
bindLoginPassword
=
ref
(
''
)
const
legacyPendingOAuthToken
=
ref
(
''
)
const
legacyPendingOAuthToken
=
ref
(
''
)
const
accountActionError
=
ref
(
''
)
const
accountActionError
=
ref
(
''
)
const
canReturnToCreateAccount
=
ref
(
false
)
const
needsTotpChallenge
=
ref
(
false
)
const
needsTotpChallenge
=
ref
(
false
)
const
totpTempToken
=
ref
(
''
)
const
totpTempToken
=
ref
(
''
)
const
totpCode
=
ref
(
''
)
const
totpCode
=
ref
(
''
)
...
@@ -340,12 +412,17 @@ const totpUserEmailMasked = ref('')
...
@@ -340,12 +412,17 @@ const totpUserEmailMasked = ref('')
const
bindSuccessMessage
=
t
(
'
profile.authBindings.bindSuccess
'
)
const
bindSuccessMessage
=
t
(
'
profile.authBindings.bindSuccess
'
)
const
providerName
=
'
WeChat
'
const
providerName
=
'
WeChat
'
const
showBackToChooser
=
computed
(
()
=>
pendingAccountAction
.
value
===
'
create_account
'
||
pendingAccountAction
.
value
===
'
bind_login
'
)
const
needsCreateAccount
=
computed
(()
=>
pendingAccountAction
.
value
===
'
create_account
'
)
const
needsCreateAccount
=
computed
(()
=>
pendingAccountAction
.
value
===
'
create_account
'
)
const
needsBindLogin
=
computed
(()
=>
pendingAccountAction
.
value
===
'
bind_login
'
)
const
needsBindLogin
=
computed
(()
=>
pendingAccountAction
.
value
===
'
bind_login
'
)
const
hasCurrentAuthToken
=
computed
(()
=>
Boolean
(
getAuthToken
()))
const
hasCurrentAuthToken
=
computed
(()
=>
Boolean
(
getAuthToken
()))
type
PendingWeChatCompletion
=
PendingOAuthExchangeResponse
&
{
type
PendingWeChatCompletion
=
PendingOAuthExchangeResponse
&
{
step
?:
string
step
?:
string
status
?:
string
state
?:
string
pending_email
?:
string
pending_email
?:
string
resolved_email
?:
string
resolved_email
?:
string
existing_account_email
?:
string
existing_account_email
?:
string
...
@@ -489,11 +566,6 @@ function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_use
...
@@ -489,11 +566,6 @@ function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_use
intent
,
intent
,
}
)
}
)
const
email
=
existingAccountEmail
.
value
.
trim
()
if
(
email
)
{
params
.
set
(
'
email
'
,
email
)
}
return
`${normalized
}
/auth/oauth/wechat/start?${params.toString()
}
`
return
`${normalized
}
/auth/oauth/wechat/start?${params.toString()
}
`
}
}
...
@@ -502,6 +574,7 @@ function buildExistingAccountResumePath(): string | null {
...
@@ -502,6 +574,7 @@ function buildExistingAccountResumePath(): string | null {
if
(
!
mode
)
{
if
(
!
mode
)
{
return
null
return
null
}
}
const
params
=
new
URLSearchParams
({
const
params
=
new
URLSearchParams
({
wechat_bind_existing
:
'
1
'
,
wechat_bind_existing
:
'
1
'
,
redirect
:
resolveRedirectTarget
(),
redirect
:
resolveRedirectTarget
(),
...
@@ -538,26 +611,31 @@ function serializeAdoptionDecision(decision: OAuthAdoptionDecision): Record<stri
...
@@ -538,26 +611,31 @@ function serializeAdoptionDecision(decision: OAuthAdoptionDecision): Record<stri
return
payload
return
payload
}
}
async
function
handle
ExistingAccountBinding
()
{
async
function
handle
BindCurrentAccount
()
{
const
unavailableMessage
=
resolveConfiguredWeChatOAuthMode
()
===
null
const
unavailableMessage
=
resolveConfiguredWeChatOAuthMode
()
===
null
?
resolveWeChatOAuthUnavailableMessage
()
?
resolveWeChatOAuthUnavailableMessage
()
:
''
:
''
if
(
getAuthToken
())
{
const
startURL
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
const
startURL
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
if
(
!
startURL
)
{
if
(
!
startURL
)
{
errorMessage
.
value
=
unavailableMessage
||
resolveWeChatOAuthUnavailableMessage
()
errorMessage
.
value
=
unavailableMessage
||
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
appStore
.
showError
(
errorMessage
.
value
)
return
return
}
}
prepareOAuthBindAccessTokenCookie
()
prepareOAuthBindAccessTokenCookie
()
window
.
location
.
href
=
startURL
window
.
location
.
href
=
startURL
}
async
function
handleExistingAccountBinding
()
{
if
(
getAuthToken
())
{
await
handleBindCurrentAccount
()
return
return
}
}
const
resumePath
=
buildExistingAccountResumePath
()
const
resumePath
=
buildExistingAccountResumePath
()
if
(
!
resumePath
)
{
if
(
!
resumePath
)
{
errorMessage
.
value
=
unavailableMessage
||
resolveWeChatOAuthUnavailableMessage
()
errorMessage
.
value
=
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
appStore
.
showError
(
errorMessage
.
value
)
return
return
}
}
...
@@ -606,17 +684,29 @@ function extractPendingAccountEmail(completion: PendingWeChatCompletion): string
...
@@ -606,17 +684,29 @@ function extractPendingAccountEmail(completion: PendingWeChatCompletion): string
function
resolvePendingAccountAction
(
function
resolvePendingAccountAction
(
completion
:
PendingWeChatCompletion
completion
:
PendingWeChatCompletion
):
'
none
'
|
'
create_account
'
|
'
bind_login
'
{
):
'
none
'
|
'
choice
'
|
'
create_account
'
|
'
bind_login
'
{
const
raw
=
normalizedPendingState
(
completion
.
step
||
completion
.
error
||
completion
.
intent
)
const
raw
=
normalizedPendingState
(
completion
.
step
||
completion
.
status
||
completion
.
state
||
completion
.
error
||
completion
.
intent
)
if
(
raw
===
'
choice
'
||
raw
===
'
choose_account_action_required
'
||
raw
===
'
choose_account_action
'
||
raw
===
'
choose_account
'
||
raw
===
'
choose
'
||
raw
===
'
existing_account
'
||
raw
===
'
existing_account_required
'
||
raw
===
'
existing_account_binding_required
'
||
raw
===
'
adopt_existing_user_by_email
'
)
{
return
'
choice
'
}
if
(
raw
===
'
email_required
'
||
raw
===
'
create_account_required
'
||
raw
===
'
create_account
'
)
{
if
(
raw
===
'
email_required
'
||
raw
===
'
create_account_required
'
||
raw
===
'
create_account
'
)
{
return
'
create_account
'
return
'
create_account
'
}
}
if
(
if
(
raw
===
'
bind_login_required
'
||
raw
===
'
bind_login_required
'
||
raw
===
'
bind_login
'
||
raw
===
'
bind_login
'
raw
===
'
existing_account_binding_required
'
||
raw
===
'
existing_account_required
'
||
raw
===
'
adopt_existing_user_by_email
'
)
{
)
{
return
'
bind_login
'
return
'
bind_login
'
}
}
...
@@ -627,6 +717,7 @@ function applyPendingAccountAction(completion: PendingWeChatCompletion) {
...
@@ -627,6 +717,7 @@ function applyPendingAccountAction(completion: PendingWeChatCompletion) {
const
action
=
resolvePendingAccountAction
(
completion
)
const
action
=
resolvePendingAccountAction
(
completion
)
pendingAccountAction
.
value
=
action
pendingAccountAction
.
value
=
action
accountActionError
.
value
=
''
accountActionError
.
value
=
''
needsChooser
.
value
=
false
needsTotpChallenge
.
value
=
false
needsTotpChallenge
.
value
=
false
totpTempToken
.
value
=
''
totpTempToken
.
value
=
''
totpCode
.
value
=
''
totpCode
.
value
=
''
...
@@ -634,20 +725,22 @@ function applyPendingAccountAction(completion: PendingWeChatCompletion) {
...
@@ -634,20 +725,22 @@ function applyPendingAccountAction(completion: PendingWeChatCompletion) {
totpUserEmailMasked
.
value
=
''
totpUserEmailMasked
.
value
=
''
const
email
=
extractPendingAccountEmail
(
completion
)
const
email
=
extractPendingAccountEmail
(
completion
)
if
(
action
===
'
create_account
'
)
{
pendingAccountEmail
.
value
=
email
pendingAccountEmail
.
value
=
email
canReturnToC
reate
A
ccount
.
value
=
true
if
(
action
===
'
c
reate
_a
ccount
'
)
{
return
return
}
}
if
(
action
===
'
bind_login
'
)
{
if
(
action
===
'
bind_login
'
)
{
bindLoginEmail
.
value
=
email
bindLoginEmail
.
value
=
email
bindLoginPassword
.
value
=
''
bindLoginPassword
.
value
=
''
canReturnToCreateAccount
.
value
=
true
return
return
}
}
canReturnToCreateAccount
.
value
=
false
if
(
action
===
'
choice
'
)
{
needsChooser
.
value
=
true
bindLoginPassword
.
value
=
''
return
}
}
}
function
applyTotpChallenge
(
completion
:
PendingWeChatCompletion
):
boolean
{
function
applyTotpChallenge
(
completion
:
PendingWeChatCompletion
):
boolean
{
...
@@ -656,6 +749,7 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
...
@@ -656,6 +749,7 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
}
}
pendingAccountAction
.
value
=
'
none
'
pendingAccountAction
.
value
=
'
none
'
needsChooser
.
value
=
false
needsInvitation
.
value
=
false
needsInvitation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
needsAdoptionConfirmation
.
value
=
false
needsTotpChallenge
.
value
=
true
needsTotpChallenge
.
value
=
true
...
@@ -669,18 +763,26 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
...
@@ -669,18 +763,26 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
function
switchToBindLoginMode
(
nextEmail
?:
string
)
{
function
switchToBindLoginMode
(
nextEmail
?:
string
)
{
pendingAccountAction
.
value
=
'
bind_login
'
pendingAccountAction
.
value
=
'
bind_login
'
needsChooser
.
value
=
false
bindLoginEmail
.
value
=
bindLoginEmail
.
value
.
trim
()
||
nextEmail
?.
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
}
}
function
switchToCreateAccountMode
()
{
function
switchToCreateAccountMode
()
{
pendingAccountAction
.
value
=
'
create_account
'
pendingAccountAction
.
value
=
'
create_account
'
needsChooser
.
value
=
false
pendingAccountEmail
.
value
=
pendingAccountEmail
.
value
.
trim
()
||
bindLoginEmail
.
value
.
trim
()
pendingAccountEmail
.
value
=
pendingAccountEmail
.
value
.
trim
()
||
bindLoginEmail
.
value
.
trim
()
accountActionError
.
value
=
''
accountActionError
.
value
=
''
}
}
function
switchToChoiceMode
()
{
pendingAccountAction
.
value
=
'
choice
'
needsChooser
.
value
=
true
bindLoginPassword
.
value
=
''
accountActionError
.
value
=
''
}
function
getRequestErrorMessage
(
error
:
unknown
,
fallback
:
string
):
string
{
function
getRequestErrorMessage
(
error
:
unknown
,
fallback
:
string
):
string
{
const
err
=
error
as
{
message
?:
string
;
response
?:
{
data
?:
{
detail
?:
string
;
message
?:
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
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
...
@@ -705,7 +807,9 @@ function isCreateAccountRecoveryError(error: unknown): boolean {
...
@@ -705,7 +807,9 @@ function isCreateAccountRecoveryError(error: unknown): boolean {
return
states
.
includes
(
'
email_exists
'
)
||
return
states
.
includes
(
'
email_exists
'
)
||
states
.
includes
(
'
bind_login_required
'
)
||
states
.
includes
(
'
bind_login_required
'
)
||
states
.
includes
(
'
bind_login
'
)
||
states
.
includes
(
'
bind_login
'
)
||
states
.
includes
(
'
adopt_existing_user_by_email
'
)
states
.
includes
(
'
adopt_existing_user_by_email
'
)
||
states
.
includes
(
'
existing_account_required
'
)
||
states
.
includes
(
'
existing_account_binding_required
'
)
}
}
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
...
@@ -818,7 +922,10 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
...
@@ -818,7 +922,10 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
if
(
isCreateAccountRecoveryError
(
e
))
{
if
(
isCreateAccountRecoveryError
(
e
))
{
switchToBindLoginMode
(
payload
.
email
)
switchToChoiceMode
()
pendingAccountEmail
.
value
=
payload
.
email
.
trim
()
bindLoginEmail
.
value
=
payload
.
email
.
trim
()
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
return
return
}
}
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
...
@@ -878,20 +985,15 @@ onMounted(async () => {
...
@@ -878,20 +985,15 @@ onMounted(async () => {
}
}
if
(
typeof
route
.
query
.
email
===
'
string
'
)
{
if
(
typeof
route
.
query
.
email
===
'
string
'
)
{
existingAccountEmail
.
value
=
route
.
query
.
email
const
email
=
route
.
query
.
email
.
trim
()
existingAccountEmail
.
value
=
email
bindLoginEmail
.
value
=
email
pendingAccountEmail
.
value
=
email
}
}
if
(
route
.
query
.
wechat_bind_existing
===
'
1
'
)
{
if
(
route
.
query
.
wechat_bind_existing
===
'
1
'
)
{
if
(
getAuthToken
())
{
if
(
getAuthToken
())
{
const
startURL
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
await
handleBindCurrentAccount
()
if
(
!
startURL
)
{
errorMessage
.
value
=
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
return
}
prepareOAuthBindAccessTokenCookie
()
window
.
location
.
href
=
startURL
return
return
}
}
...
...
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