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

Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
cneicy committed 2026-09-26 04:49:11 +08:00
1 parent c2b44332d6
commit a5f7b8e586
828 files changed
+122253 -13702

No files matched your search

+89 -34
View File
@@ -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) {