feat: sync deployed v1.0.0-rc.38-custom.42
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
c2b44332d6
commit
a5f7b8e586
828 files changed
+122253
-13702
No files matched your search
+89
-34
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -292,7 +293,7 @@ func HandleOAuth(c *gin.Context) {
|
||||
|
||||
switch flow.Intent {
|
||||
case model.AuthFlowIntentLogin:
|
||||
handleOAuthLogin(c, provider, oauthUser, flow)
|
||||
handleOAuthLogin(c, provider, oauthUser, token, flow)
|
||||
case model.AuthFlowIntentVerify:
|
||||
handleOAuthVerification(c, providerName, oauthUser, flow)
|
||||
}
|
||||
@@ -314,14 +315,14 @@ func handleOAuthVerification(c *gin.Context, provider string, oauthUser *oauth.O
|
||||
common.ApiSuccess(c, proof)
|
||||
}
|
||||
|
||||
func handleOAuthLogin(c *gin.Context, provider oauth.Provider, oauthUser *oauth.OAuthUser, flow *model.AuthFlow) {
|
||||
func handleOAuthLogin(c *gin.Context, provider oauth.Provider, oauthUser *oauth.OAuthUser, token *oauth.OAuthToken, flow *model.AuthFlow) {
|
||||
// 7. Find or create user
|
||||
var payload oauthFlowPayload
|
||||
if err := common.UnmarshalJsonStr(flow.Payload, &payload); err != nil {
|
||||
writeSecurityOperationError(c, err)
|
||||
return
|
||||
}
|
||||
user, err := findOrCreateOAuthUser(c, provider, oauthUser, payload.AffiliateCode)
|
||||
user, migration, err := findOrCreateOAuthUser(c, provider, oauthUser, token, payload.AffiliateCode)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrEmailAlreadyTaken) {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
|
||||
@@ -334,6 +335,8 @@ func handleOAuthLogin(c *gin.Context, provider oauth.Provider, oauthUser *oauth.
|
||||
common.ApiErrorI18n(c, i18n.MsgUserRegisterDisabled)
|
||||
case *OAuthEmailAlreadyTakenError:
|
||||
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
|
||||
case *OAuthLegacyBindingNotConfirmedError:
|
||||
common.ApiErrorI18n(c, i18n.MsgOAuthNotAutoLinked, providerParams(provider.GetName()))
|
||||
default:
|
||||
writeSecurityOperationError(c, err)
|
||||
}
|
||||
@@ -347,7 +350,7 @@ func handleOAuthLogin(c *gin.Context, provider oauth.Provider, oauthUser *oauth.
|
||||
}
|
||||
|
||||
// 9. Setup login
|
||||
setupLogin(user, c)
|
||||
setupLogin(user, migration, c)
|
||||
}
|
||||
|
||||
// handleOAuthBind handles binding OAuth account to existing user
|
||||
@@ -377,10 +380,6 @@ func handleOAuthBind(c *gin.Context, providerName string, provider oauth.Provide
|
||||
common.ApiErrorI18n(c, i18n.MsgOAuthAlreadyBound, providerParams(provider.GetName()))
|
||||
return false, false
|
||||
}
|
||||
if legacyID, ok := oauthUser.Extra["legacy_id"].(string); ok && legacyID != "" && provider.IsUserIDTaken(legacyID) {
|
||||
common.ApiErrorI18n(c, i18n.MsgOAuthAlreadyBound, providerParams(provider.GetName()))
|
||||
return false, false
|
||||
}
|
||||
_, err = model.ConsumeAuthFlowWithAction(state, match, func(tx *gorm.DB, _ *model.AuthFlow) error {
|
||||
if providerName == "telegram" {
|
||||
return model.BindTelegramForSessionWithTx(tx, identity, oauthUser.ProviderUserID)
|
||||
@@ -404,53 +403,91 @@ func handleOAuthBind(c *gin.Context, providerName string, provider oauth.Provide
|
||||
return true, notificationFailed
|
||||
}
|
||||
|
||||
// findOrCreateOAuthUser finds existing user or creates new user
|
||||
func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *oauth.OAuthUser, affiliateCode string) (*model.User, error) {
|
||||
// findOrCreateOAuthUser finds the existing user or creates a new one. For a
|
||||
// legacy GitHub binding that still waits for the login verification, it also
|
||||
// returns the rewrite to carry into the challenge.
|
||||
func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *oauth.OAuthUser, token *oauth.OAuthToken, affiliateCode string) (*model.User, *service.LegacyGitHubMigration, error) {
|
||||
user := &model.User{}
|
||||
if provider.ProviderUserIDColumn() == "telegram_id" {
|
||||
err := provider.FillUserByProviderID(user, oauthUser.ProviderUserID)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, oauth.ErrTelegramAccountNotBound
|
||||
return nil, nil, oauth.ErrTelegramAccountNotBound
|
||||
}
|
||||
return user, err
|
||||
return user, nil, err
|
||||
}
|
||||
|
||||
// Check if user already exists with new ID
|
||||
if provider.IsUserIDTaken(oauthUser.ProviderUserID) {
|
||||
err := provider.FillUserByProviderID(user, oauthUser.ProviderUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
// Check if user has been deleted
|
||||
if user.Id == 0 {
|
||||
return nil, &OAuthUserDeletedError{}
|
||||
return nil, nil, &OAuthUserDeletedError{}
|
||||
}
|
||||
return user, nil
|
||||
return user, nil, nil
|
||||
}
|
||||
|
||||
// Try to find user with legacy ID (for GitHub migration from login to numeric ID)
|
||||
if legacyID, ok := oauthUser.Extra["legacy_id"].(string); ok && legacyID != "" {
|
||||
if provider.IsUserIDTaken(legacyID) {
|
||||
err := provider.FillUserByProviderID(user, legacyID)
|
||||
// Legacy GitHub bindings stored the login name, which only points at a
|
||||
// candidate account. Values that are all digits are already numeric account
|
||||
// IDs and never take part in the comparison.
|
||||
legacyID, _ := oauthUser.Extra["legacy_id"].(string)
|
||||
if strings.ContainsFunc(legacyID, func(r rune) bool { return r < '0' || r > '9' }) && provider.IsUserIDTaken(legacyID) {
|
||||
if err := provider.FillUserByProviderID(user, legacyID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if user.Id != 0 {
|
||||
state, err := model.GetUserVerificationState(user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if user.Id != 0 {
|
||||
// Found user with legacy ID, migrate to new ID
|
||||
common.SysLog(fmt.Sprintf("[OAuth] Migrating user %d from legacy_id=%s to new_id=%s",
|
||||
user.Id, legacyID, oauthUser.ProviderUserID))
|
||||
if err := user.UpdateGitHubId(oauthUser.ProviderUserID); err != nil {
|
||||
common.SysError(fmt.Sprintf("[OAuth] Failed to migrate user %d: %s", user.Id, err.Error()))
|
||||
// Continue with login even if migration fails
|
||||
if state.HasTwoFA || state.HasPasskey {
|
||||
// The rewrite is written in the transaction that issues the session
|
||||
// once the login verification completes.
|
||||
return user, &service.LegacyGitHubMigration{GitHubID: oauthUser.ProviderUserID, LegacyID: legacyID}, nil
|
||||
}
|
||||
// Without a second factor, one of the addresses the provider has
|
||||
// confirmed must match the account email. The list is fetched only here
|
||||
// and never recorded.
|
||||
reason, matched := "no_matching_evidence", false
|
||||
if emailProvider, ok := provider.(oauth.VerifiedEmailProvider); ok && user.Email != "" {
|
||||
emails, err := emailProvider.GetVerifiedEmails(c.Request.Context(), token)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("[OAuth] Failed to load verified emails for user %d: %s", user.Id, err.Error()))
|
||||
reason = "verified_emails_unavailable"
|
||||
}
|
||||
return user, nil
|
||||
accountEmail := model.NormalizeEmail(user.Email)
|
||||
matched = slices.ContainsFunc(emails, func(email string) bool { return model.NormalizeEmail(email) == accountEmail })
|
||||
}
|
||||
if !matched {
|
||||
recordLegacyGitHubBindingAudit(c, user, false, map[string]any{"legacy_id": legacyID, "provider_user_id": oauthUser.ProviderUserID, "reason": reason})
|
||||
return nil, nil, &OAuthLegacyBindingNotConfirmedError{}
|
||||
}
|
||||
written := false
|
||||
err = model.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
written, err = model.MigrateLegacyGitHubBindingWithTx(tx, user.Id, legacyID, oauthUser.ProviderUserID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if written {
|
||||
user.GitHubId = oauthUser.ProviderUserID
|
||||
notificationFailed := service.NotifyAccountSecurityChange(user.Email, "Login account linked: "+provider.GetName()) != nil
|
||||
recordLegacyGitHubBindingAudit(c, user, true, map[string]any{
|
||||
"legacy_id": legacyID, "provider_user_id": oauthUser.ProviderUserID,
|
||||
"verified_email_matched": true, "notification_failed": notificationFailed,
|
||||
})
|
||||
}
|
||||
return user, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// User doesn't exist, create new user if registration is enabled
|
||||
if !common.RegisterEnabled {
|
||||
return nil, &OAuthRegistrationDisabledError{}
|
||||
return nil, nil, &OAuthRegistrationDisabledError{}
|
||||
}
|
||||
|
||||
// Set up new user
|
||||
@@ -476,9 +513,9 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
|
||||
user.Email = model.NormalizeEmail(oauthUser.Email)
|
||||
if err := model.EnsureEmailAvailable(user.Email, 0); err != nil {
|
||||
if errors.Is(err, model.ErrEmailAlreadyTaken) {
|
||||
return nil, &OAuthEmailAlreadyTakenError{}
|
||||
return nil, nil, &OAuthEmailAlreadyTakenError{}
|
||||
}
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
user.Role = common.RoleCommonUser
|
||||
@@ -512,7 +549,7 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Perform post-transaction tasks (logs, sidebar config, inviter rewards)
|
||||
@@ -541,14 +578,24 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Perform post-transaction tasks
|
||||
user.FinalizeOAuthUserCreation(inviterId)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
return user, nil, nil
|
||||
}
|
||||
|
||||
// recordLegacyGitHubBindingAudit records the outcome of a legacy GitHub binding
|
||||
// rewrite as an account binding event. No session exists yet on the login path,
|
||||
// so the row carries the user's own role instead of the request context.
|
||||
func recordLegacyGitHubBindingAudit(c *gin.Context, user *model.User, success bool, params map[string]any) {
|
||||
params["provider"], params["legacy_migration"], params["success"] = "github", true, success
|
||||
model.RecordOperationAuditLog(user.Id, user.Role, auditContentEN("user.binding_bind", params), c.ClientIP(), "user.binding_bind", params, nil, &model.AuditRequestInfo{
|
||||
Method: c.Request.Method, Route: c.FullPath(), Path: c.FullPath(), Status: c.Writer.Status(), Success: success,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// Error types for OAuth
|
||||
@@ -570,6 +617,14 @@ func (e *OAuthEmailAlreadyTakenError) Error() string {
|
||||
return "email is already in use"
|
||||
}
|
||||
|
||||
// OAuthLegacyBindingNotConfirmedError reports a legacy GitHub binding match that
|
||||
// neither a second factor nor a confirmed provider email backed.
|
||||
type OAuthLegacyBindingNotConfirmedError struct{}
|
||||
|
||||
func (e *OAuthLegacyBindingNotConfirmedError) Error() string {
|
||||
return "legacy binding was not confirmed"
|
||||
}
|
||||
|
||||
// handleOAuthError handles OAuth errors and returns translated message
|
||||
func handleOAuthError(c *gin.Context, err error) {
|
||||
switch e := err.(type) {
|
||||
|
||||
Reference in new issue
Block a user