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.
174 lines
6.3 KiB
Go
174 lines
6.3 KiB
Go
package model
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var (
|
|
ErrCurrentPasswordInvalid = errors.New("Current password is incorrect.")
|
|
ErrAccountPasswordState = errors.New("Password settings have changed. Reload the page and try again.")
|
|
ErrLastLoginMethod = errors.New("Add another login method before unlinking this account.")
|
|
ErrAccountBindingChanged = errors.New("Account bindings have changed. Start this operation again.")
|
|
ErrCannotDeleteRootUser = errors.New("The root account cannot be deleted.")
|
|
)
|
|
|
|
// ChangeUserPassword rechecks the authorized session, password state and current
|
|
// password under the same user lock as the credential update. Only self-profile
|
|
// fields are accepted; a stale snapshot cannot restore roles or account status.
|
|
func ChangeUserPassword(identity AuthSessionIdentity, update *User, firstPassword bool) error {
|
|
hash, err := common.HashAccountPassword(update.Password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return DB.Transaction(func(tx *gorm.DB) error {
|
|
if err := ValidateAuthSessionWithTx(tx, identity); err != nil {
|
|
return err
|
|
}
|
|
var current User
|
|
if err := tx.First(¤t, identity.UserID).Error; err != nil {
|
|
return err
|
|
}
|
|
if firstPassword != (current.Password == "") {
|
|
return ErrAccountPasswordState
|
|
}
|
|
if !firstPassword && !common.ValidatePasswordAndHash(update.OriginalPassword, current.Password) {
|
|
return ErrCurrentPasswordInvalid
|
|
}
|
|
if current.Password != "" && common.ValidatePasswordAndHash(update.Password, current.Password) {
|
|
return common.ErrAccountPasswordSame
|
|
}
|
|
changes := map[string]any{"password": hash}
|
|
if update.Username != "" {
|
|
changes["username"] = update.Username
|
|
}
|
|
if update.DisplayName != "" {
|
|
changes["display_name"] = update.DisplayName
|
|
}
|
|
if _, err := IncrementUserAuthVersionWithTx(tx, identity.UserID); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&User{}).Where("id = ?", identity.UserID).Updates(changes).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.First(update, identity.UserID).Error
|
|
})
|
|
}
|
|
|
|
// AccountLoginMethods is a snapshot of administrator-enabled login mechanisms.
|
|
// Enrollment is checked inside the transaction, not trusted from this snapshot.
|
|
type AccountLoginMethods struct {
|
|
Password bool
|
|
Passkey bool
|
|
WeChat bool
|
|
OAuthColumns []string
|
|
CustomProviderIDs []int
|
|
}
|
|
|
|
func UnbindUserOAuthForSession(identity AuthSessionIdentity, providerID int, enabled AccountLoginMethods) error {
|
|
return DB.Transaction(func(tx *gorm.DB) error {
|
|
if err := ValidateAuthSessionWithTx(tx, identity); err != nil {
|
|
return err
|
|
}
|
|
var current User
|
|
if err := tx.First(¤t, identity.UserID).Error; err != nil {
|
|
return err
|
|
}
|
|
var binding UserOAuthBinding
|
|
if err := tx.Where("user_id = ? AND provider_id = ?", identity.UserID, providerID).First(&binding).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return ErrAccountBindingChanged
|
|
}
|
|
return err
|
|
}
|
|
hasLogin := enabled.Password && current.Password != "" || enabled.WeChat && current.WeChatId != ""
|
|
columns := map[string]string{
|
|
"github_id": current.GitHubId, "discord_id": current.DiscordId,
|
|
"oidc_id": current.OidcId, "linux_do_id": current.LinuxDOId, "telegram_id": current.TelegramId,
|
|
}
|
|
for _, column := range enabled.OAuthColumns {
|
|
hasLogin = hasLogin || columns[column] != ""
|
|
}
|
|
if !hasLogin && enabled.Passkey {
|
|
var count int64
|
|
if err := tx.Model(&PasskeyCredential{}).Where("user_id = ?", identity.UserID).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
hasLogin = count > 0
|
|
}
|
|
if !hasLogin && len(enabled.CustomProviderIDs) > 0 {
|
|
var count int64
|
|
if err := tx.Model(&UserOAuthBinding{}).Where("user_id = ? AND provider_id <> ? AND provider_id IN ?", identity.UserID, providerID, enabled.CustomProviderIDs).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
hasLogin = count > 0
|
|
}
|
|
if !hasLogin {
|
|
return ErrLastLoginMethod
|
|
}
|
|
return tx.Delete(&binding).Error
|
|
})
|
|
}
|
|
|
|
func UpdateUserBindColumnForSessionWithTx(tx *gorm.DB, identity AuthSessionIdentity, column, value string) error {
|
|
if !userBindColumns[column] || value == "" {
|
|
return ErrAccountBindingChanged
|
|
}
|
|
if err := ValidateAuthSessionWithTx(tx, identity); err != nil {
|
|
return err
|
|
}
|
|
// Preserve the existing ownership check within the transaction and update
|
|
// only the chosen binding column, never a complete user snapshot.
|
|
var count int64
|
|
if err := tx.Model(&User{}).Where(column+" = ? AND id <> ?", value, identity.UserID).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count != 0 {
|
|
return ErrExternalIdentityAlreadyClaimed
|
|
}
|
|
return tx.Model(&User{}).Where("id = ?", identity.UserID).Update(column, value).Error
|
|
}
|
|
|
|
func UpdateUserOAuthBindingForSessionWithTx(tx *gorm.DB, identity AuthSessionIdentity, providerID int, subject string) error {
|
|
if providerID <= 0 || subject == "" || len(subject) > 256 {
|
|
return ErrAccountBindingChanged
|
|
}
|
|
if err := ValidateAuthSessionWithTx(tx, identity); err != nil {
|
|
return err
|
|
}
|
|
var binding UserOAuthBinding
|
|
err := tx.Where("user_id = ? AND provider_id = ?", identity.UserID, providerID).First(&binding).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return CreateUserOAuthBindingWithTx(tx, &UserOAuthBinding{UserId: identity.UserID, ProviderId: providerID, ProviderUserId: subject})
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The existing unique provider/subject index rejects concurrent ownership.
|
|
return tx.Model(&binding).Update("provider_user_id", subject).Error
|
|
}
|
|
|
|
// MigrateLegacyGitHubBindingWithTx rewrites a login-name GitHub binding to the
|
|
// numeric account ID once the login flow has confirmed the account. It reports
|
|
// whether the row was rewritten; a row whose binding changed since it was read
|
|
// is left untouched.
|
|
func MigrateLegacyGitHubBindingWithTx(tx *gorm.DB, userID int, legacyID, gitHubID string) (bool, error) {
|
|
if userID <= 0 || legacyID == "" || gitHubID == "" {
|
|
return false, ErrAccountBindingChanged
|
|
}
|
|
var count int64
|
|
if err := tx.Model(&User{}).Where("github_id = ? AND id <> ?", gitHubID, userID).Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
if count != 0 {
|
|
return false, ErrExternalIdentityAlreadyClaimed
|
|
}
|
|
result := tx.Model(&User{}).Where("id = ? AND github_id = ?", userID, legacyID).Update("github_id", gitHubID)
|
|
if result.Error != nil {
|
|
return false, result.Error
|
|
}
|
|
return result.RowsAffected == 1, nil
|
|
}
|