152 lines
5.4 KiB
Go
152 lines
5.4 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
|
|
}
|