feat: Users API, ownership transfer and removal of the legacy role string
This commit is contained in:
1 parent
094df63a7f
commit
80f7037d3a
124 files changed
+4644
-824
No files matched your search
@@ -1,7 +1,7 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/domain/role"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -9,8 +9,15 @@ import (
|
||||
|
||||
func BackfillCreatedBy(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
ownerRoleID, seeded, err := builtInRoleID(tx, role.OwnerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !seeded {
|
||||
return nil
|
||||
}
|
||||
var owner entity.User
|
||||
result := tx.Where("role = ?", string(domainUser.RoleOwner)).Order("id asc").Limit(1).Find(&owner)
|
||||
result := tx.Where("role_id = ?", ownerRoleID).Order("id asc").Limit(1).Find(&owner)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/role"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func BackfillInviteRoles(db *gorm.DB) error {
|
||||
if !legacyRoleColumnExists(db) {
|
||||
return nil
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
done, err := markerApplied(tx, inviteRolesMarker)
|
||||
if err != nil || done {
|
||||
return err
|
||||
}
|
||||
adminID, err := requireBuiltInRoleID(tx, role.AdminName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&entity.Invite{}).Where("role_id IS NULL OR role_id = 0").UpdateColumn("role_id", adminID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return applyMarker(tx, inviteRolesMarker)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/role"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const (
|
||||
usersTable = "users"
|
||||
legacyRoleColumn = "role"
|
||||
legacyOwnerValue = "owner"
|
||||
legacyAdminValue = "admin"
|
||||
legacyUnsetValue = ""
|
||||
unassignedRoleCond = "(role_id IS NULL OR role_id = 0)"
|
||||
)
|
||||
|
||||
func legacyRoleColumnExists(db *gorm.DB) bool {
|
||||
return db.Migrator().HasColumn(usersTable, legacyRoleColumn)
|
||||
}
|
||||
|
||||
func requireBuiltInRoleID(tx *gorm.DB, name string) (uint32, error) {
|
||||
id, found, err := builtInRoleID(tx, name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !found {
|
||||
return 0, role.ErrRoleNotFound
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func BackfillLegacyUserRoles(db *gorm.DB) error {
|
||||
if !legacyRoleColumnExists(db) {
|
||||
return nil
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
done, err := markerApplied(tx, legacyUserRolesMarker)
|
||||
if err != nil || done {
|
||||
return err
|
||||
}
|
||||
ownerID, err := requireBuiltInRoleID(tx, role.OwnerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adminID, err := requireBuiltInRoleID(tx, role.AdminName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writerID, err := requireBuiltInRoleID(tx, role.WriterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacy := clause.Column{Name: legacyRoleColumn}
|
||||
assign := func(roleID uint32, condition string, args ...any) error {
|
||||
return tx.Table(usersTable).Where(unassignedRoleCond).Where(condition, args...).UpdateColumn("role_id", roleID).Error
|
||||
}
|
||||
var owners []uint32
|
||||
if err := tx.Table(usersTable).Where(unassignedRoleCond).Where("? = ?", legacy, legacyOwnerValue).Order("id asc").Pluck("id", &owners).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(owners) > 0 {
|
||||
if err := assign(ownerID, "id = ?", owners[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := assign(adminID, "? = ?", legacy, legacyOwnerValue); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assign(adminID, "? = ?", legacy, legacyAdminValue); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(owners) == 0 {
|
||||
var lowest []uint32
|
||||
if err := tx.Model(&entity.User{}).Order("id asc").Limit(1).Pluck("id", &lowest).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(lowest) > 0 {
|
||||
if err := assign(ownerID, "? = ? AND id = ?", legacy, legacyUnsetValue, lowest[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := assign(adminID, "? = ?", legacy, legacyUnsetValue); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assign(writerID, "1 = 1"); err != nil {
|
||||
return err
|
||||
}
|
||||
return applyMarker(tx, legacyUserRolesMarker)
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func Migrate(db *gorm.DB, publicURL string) error {
|
||||
err := tableOptionsFor(db).AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}, &models.PurgeSettings{}, &models.AuditEvent{}, &models.AuditSettings{}, &models.ImageVariant{}, &models.ImageVariantFile{}, &models.ImageVariantRejectionDay{}, &models.Site{}, &models.Author{}, &models.ArticleAuthor{}, &models.ContentType{}, &models.Entry{}, &models.RevokedSession{}, &models.Role{}, &models.RolePermission{})
|
||||
err := tableOptionsFor(db).AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}, &models.PurgeSettings{}, &models.AuditEvent{}, &models.AuditSettings{}, &models.ImageVariant{}, &models.ImageVariantFile{}, &models.ImageVariantRejectionDay{}, &models.Site{}, &models.Author{}, &models.ArticleAuthor{}, &models.ContentType{}, &models.Entry{}, &models.RevokedSession{}, &models.Role{}, &models.RolePermission{}, &models.MigrationMarker{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -17,10 +17,10 @@ func Migrate(db *gorm.DB, publicURL string) error {
|
||||
if err := SeedBuiltInRoles(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BackfillUserRoles(db); err != nil {
|
||||
if err := BackfillLegacyUserRoles(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BackfillUserRoleIDs(db); err != nil {
|
||||
if err := BackfillInviteRoles(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BackfillCreatedBy(db); err != nil {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyUserRolesMarker = "legacy_user_roles"
|
||||
inviteRolesMarker = "invite_roles"
|
||||
)
|
||||
|
||||
func markerApplied(tx *gorm.DB, name string) (bool, error) {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.MigrationMarker{}).Where("name = ?", name).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func applyMarker(tx *gorm.DB, name string) error {
|
||||
return tx.Create(&entity.MigrationMarker{Name: name, AppliedAt: time.Now()}).Error
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func BackfillUserRoles(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var firstUser entity.User
|
||||
result := tx.Where("role = ''").Order("id asc").Limit(1).Find(&firstUser)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lowest entity.User
|
||||
if err := tx.Order("id asc").First(&lowest).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if lowest.Role == "" {
|
||||
if err := tx.Model(&entity.User{}).Where("id = ?", lowest.ID).Update("role", string(domainUser.RoleOwner)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Model(&entity.User{}).Where("role = ''").Update("role", string(domainUser.RoleAdmin)).Error
|
||||
})
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/role"
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var legacyRoleNames = map[domainUser.Role]string{
|
||||
domainUser.RoleOwner: role.OwnerName,
|
||||
domainUser.RoleAdmin: role.AdminName,
|
||||
}
|
||||
|
||||
func BackfillUserRoleIDs(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
for legacy, name := range legacyRoleNames {
|
||||
roleID, found, err := builtInRoleID(tx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
err = tx.Model(&entity.User{}).
|
||||
Where("role = ? AND (role_id IS NULL OR role_id = 0)", string(legacy)).
|
||||
UpdateColumn("role_id", roleID).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ func InviteToDomain(m models.Invite) invite.Invite {
|
||||
HashedCode: m.HashedCode,
|
||||
ExpiresAt: m.ExpiresAt,
|
||||
InvitedBy: m.InvitedBy,
|
||||
RoleID: nullableIDToDomain(m.RoleID),
|
||||
AcceptedAt: m.AcceptedAt,
|
||||
CreatedAt: m.CreatedAt,
|
||||
}
|
||||
@@ -24,6 +25,7 @@ func InviteToModel(i invite.Invite) models.Invite {
|
||||
HashedCode: i.HashedCode,
|
||||
ExpiresAt: i.ExpiresAt,
|
||||
InvitedBy: i.InvitedBy,
|
||||
RoleID: DomainToNullableID(i.RoleID),
|
||||
AcceptedAt: i.AcceptedAt,
|
||||
CreatedAt: i.CreatedAt,
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func nullableIDToDomain(value *uint32) uint32 {
|
||||
return *value
|
||||
}
|
||||
|
||||
func domainToNullableID(value uint32) *uint32 {
|
||||
func DomainToNullableID(value uint32) *uint32 {
|
||||
if value == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -42,7 +42,6 @@ func UserToDomain(m models.User) user.User {
|
||||
Email: m.Email,
|
||||
DisplayName: m.DisplayName,
|
||||
IsVerified: m.IsVerified,
|
||||
Role: user.Role(m.Role),
|
||||
RoleID: nullableIDToDomain(m.RoleID),
|
||||
VerificationCode: nullableStringToDomain(m.VerificationCode),
|
||||
VerificationExpiration: m.VerificationExpiration,
|
||||
@@ -61,8 +60,7 @@ func UserToModel(a user.User) models.User {
|
||||
Email: a.Email,
|
||||
DisplayName: a.DisplayName,
|
||||
IsVerified: a.IsVerified,
|
||||
Role: string(a.Role),
|
||||
RoleID: domainToNullableID(a.RoleID),
|
||||
RoleID: DomainToNullableID(a.RoleID),
|
||||
VerificationCode: DomainToNullableString(a.VerificationCode),
|
||||
VerificationExpiration: a.VerificationExpiration,
|
||||
PasswordResetExpiration: a.PasswordResetExpiration,
|
||||
|
||||
@@ -7,7 +7,8 @@ type Invite struct {
|
||||
Email string `gorm:"index;not null;size:255"`
|
||||
HashedCode string `gorm:"not null"`
|
||||
ExpiresAt time.Time
|
||||
InvitedBy uint32 `gorm:"not null"`
|
||||
InvitedBy uint32 `gorm:"not null"`
|
||||
RoleID *uint32 `gorm:"index"`
|
||||
AcceptedAt *time.Time
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type MigrationMarker struct {
|
||||
Name string `gorm:"primaryKey;size:64"`
|
||||
AppliedAt time.Time `gorm:"not null"`
|
||||
}
|
||||
@@ -12,7 +12,6 @@ type User struct {
|
||||
Email string `gorm:"unique;not null;size:255"`
|
||||
DisplayName string `gorm:"not null;default:'';size:255"`
|
||||
IsVerified bool `gorm:"default=false;not null"`
|
||||
Role string `gorm:"not null;default:'';size:32"`
|
||||
RoleID *uint32 `gorm:"index"`
|
||||
VerificationCode *string `gorm:"unique;size:255"`
|
||||
VerificationExpiration time.Time
|
||||
|
||||
@@ -145,8 +145,8 @@ func (r *RoleRepository) Delete(id uint32) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&entity.Role{}, id).Error; err != nil {
|
||||
return mapNotFoundErr(err, role.ErrRoleNotFound)
|
||||
}
|
||||
var members int64
|
||||
if err := tx.Model(&entity.User{}).Where("role_id = ?", id).Count(&members).Error; err != nil {
|
||||
members, err := countMembers(tx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if members > 0 {
|
||||
@@ -159,24 +159,68 @@ func (r *RoleRepository) Delete(id uint32) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RoleRepository) ReassignMembers(from uint32, to uint32) (int, error) {
|
||||
moved := r.db.Model(&entity.User{}).
|
||||
Where("role_id = ? AND EXISTS (SELECT 1 FROM roles WHERE id = ?)", from, to).
|
||||
UpdateColumn("role_id", to)
|
||||
if moved.Error != nil {
|
||||
return 0, moved.Error
|
||||
}
|
||||
if moved.RowsAffected > 0 {
|
||||
return int(moved.RowsAffected), nil
|
||||
}
|
||||
var targets int64
|
||||
if err := r.db.Model(&entity.Role{}).Where("id = ?", to).Count(&targets).Error; err != nil {
|
||||
func countMembers(tx *gorm.DB, id uint32) (int64, error) {
|
||||
var users, invites int64
|
||||
if err := tx.Model(&entity.User{}).Where("role_id = ?", id).Count(&users).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if targets == 0 {
|
||||
return 0, role.ErrInvalidReassign
|
||||
if err := tx.Model(&entity.Invite{}).Where("role_id = ? AND accepted_at IS NULL", id).Count(&invites).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
return users + invites, nil
|
||||
}
|
||||
|
||||
type memberRow struct {
|
||||
RoleID uint32
|
||||
Members int
|
||||
}
|
||||
|
||||
func (r *RoleRepository) MemberCounts() (map[uint32]int, error) {
|
||||
var users, invites []memberRow
|
||||
if err := r.db.Model(&entity.User{}).Select("role_id, COUNT(*) AS members").Where("role_id IS NOT NULL").Group("role_id").Scan(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.db.Model(&entity.Invite{}).Select("role_id, COUNT(*) AS members").Where("role_id IS NOT NULL AND accepted_at IS NULL").Group("role_id").Scan(&invites).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts := map[uint32]int{}
|
||||
for _, row := range append(users, invites...) {
|
||||
if row.RoleID != 0 {
|
||||
counts[row.RoleID] += row.Members
|
||||
}
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (r *RoleRepository) ReassignMembers(from uint32, to uint32) (int, error) {
|
||||
moved := 0
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
target := "EXISTS (SELECT 1 FROM roles WHERE id = ?)"
|
||||
users := tx.Model(&entity.User{}).Where("role_id = ? AND "+target, from, to).UpdateColumn("role_id", to)
|
||||
if users.Error != nil {
|
||||
return users.Error
|
||||
}
|
||||
invites := tx.Model(&entity.Invite{}).Where("role_id = ? AND accepted_at IS NULL AND "+target, from, to).UpdateColumn("role_id", to)
|
||||
if invites.Error != nil {
|
||||
return invites.Error
|
||||
}
|
||||
moved = int(users.RowsAffected + invites.RowsAffected)
|
||||
if moved > 0 {
|
||||
return nil
|
||||
}
|
||||
var targets int64
|
||||
if err := tx.Model(&entity.Role{}).Where("id = ?", to).Count(&targets).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if targets == 0 {
|
||||
return role.ErrInvalidReassign
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return moved, nil
|
||||
}
|
||||
|
||||
func (r *RoleRepository) RemoveTypedGrants(typeKey string) (int, error) {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/role"
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
@@ -34,7 +36,7 @@ func (u *UserRepository) Create(user domainUser.User) (domainUser.User, error) {
|
||||
PasswordResetCode: mappers.DomainToNullableString(user.PasswordResetCode),
|
||||
Email: user.Email,
|
||||
IsVerified: user.IsVerified,
|
||||
Role: string(user.Role),
|
||||
RoleID: mappers.DomainToNullableID(user.RoleID),
|
||||
VerificationCode: mappers.DomainToNullableString(user.VerificationCode),
|
||||
VerificationExpiration: user.VerificationExpiration,
|
||||
}
|
||||
@@ -145,14 +147,17 @@ func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string, exp
|
||||
|
||||
func (u *UserRepository) ClearVerificationCode(userId uint32) (domainUser.User, error) {
|
||||
var localUser entity.User
|
||||
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
|
||||
if err != nil {
|
||||
return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound)
|
||||
}
|
||||
|
||||
localUser.IsVerified = true
|
||||
localUser.VerificationCode = mappers.DomainToNullableString("")
|
||||
err = u.db.Save(&localUser).Error
|
||||
err := u.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&entity.User{}).First(&localUser, userId).Error; err != nil {
|
||||
return mapNotFoundErr(err, domainUser.ErrUserNotFound)
|
||||
}
|
||||
if err := refuseSecondVerifiedOwner(tx, localUser); err != nil {
|
||||
return err
|
||||
}
|
||||
localUser.IsVerified = true
|
||||
localUser.VerificationCode = mappers.DomainToNullableString("")
|
||||
return tx.Save(&localUser).Error
|
||||
})
|
||||
if err != nil {
|
||||
return domainUser.User{}, err
|
||||
}
|
||||
@@ -160,6 +165,26 @@ func (u *UserRepository) ClearVerificationCode(userId uint32) (domainUser.User,
|
||||
return mappers.UserToDomain(localUser), nil
|
||||
}
|
||||
|
||||
func refuseSecondVerifiedOwner(tx *gorm.DB, candidate entity.User) error {
|
||||
if candidate.RoleID == nil {
|
||||
return nil
|
||||
}
|
||||
var roles []entity.Role
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND built_in = ? AND name = ?", *candidate.RoleID, true, role.OwnerName).Find(&roles).Error
|
||||
if err != nil || len(roles) == 0 {
|
||||
return err
|
||||
}
|
||||
var others int64
|
||||
err = tx.Model(&entity.User{}).Where("role_id = ? AND is_verified = ? AND id <> ?", *candidate.RoleID, true, candidate.ID).Count(&others).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if others > 0 {
|
||||
return domainUser.ErrInvalidVerificationCode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserRepository) ResetPassword(userId uint32, hashedPassword string) (domainUser.User, error) {
|
||||
var localUser entity.User
|
||||
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
|
||||
@@ -195,4 +220,68 @@ func (u *UserRepository) UpdateDisplayName(userId uint32, displayName string) (d
|
||||
return mappers.UserToDomain(localUser), nil
|
||||
}
|
||||
|
||||
func (u *UserRepository) UpdateRole(id uint32, from uint32, to uint32) (domainUser.User, error) {
|
||||
err := u.db.Transaction(func(tx *gorm.DB) error {
|
||||
query := tx.Model(&entity.User{}).Where("id = ? AND EXISTS (SELECT 1 FROM roles WHERE id = ?)", id, to)
|
||||
if from == 0 {
|
||||
query = query.Where("(role_id IS NULL OR role_id = 0)")
|
||||
} else {
|
||||
query = query.Where("role_id = ?", from)
|
||||
}
|
||||
result := query.Update("role_id", to)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
return nil
|
||||
}
|
||||
return u.explainRoleRefusal(tx, id, to)
|
||||
})
|
||||
if err != nil {
|
||||
return domainUser.User{}, err
|
||||
}
|
||||
return u.Get(id)
|
||||
}
|
||||
|
||||
func (u *UserRepository) explainRoleRefusal(tx *gorm.DB, id uint32, to uint32) error {
|
||||
var users int64
|
||||
if err := tx.Model(&entity.User{}).Where("id = ?", id).Count(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if users == 0 {
|
||||
return domainUser.ErrUserNotFound
|
||||
}
|
||||
var roles int64
|
||||
if err := tx.Model(&entity.Role{}).Where("id = ?", to).Count(&roles).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if roles == 0 {
|
||||
return role.ErrRoleNotFound
|
||||
}
|
||||
return domainUser.ErrRoleChanged
|
||||
}
|
||||
|
||||
func (u *UserRepository) TransferOwnership(from uint32, to uint32, ownerRoleID uint32, formerRoleID uint32) error {
|
||||
if from == to {
|
||||
return domainUser.ErrInvalidTransfer
|
||||
}
|
||||
return u.db.Transaction(func(tx *gorm.DB) error {
|
||||
demoted := tx.Model(&entity.User{}).Where("id = ? AND role_id = ?", from, ownerRoleID).Update("role_id", formerRoleID)
|
||||
if demoted.Error != nil {
|
||||
return demoted.Error
|
||||
}
|
||||
if demoted.RowsAffected == 0 {
|
||||
return domainUser.ErrRoleChanged
|
||||
}
|
||||
promoted := tx.Model(&entity.User{}).Where("id = ? AND is_verified = ?", to, true).Update("role_id", ownerRoleID)
|
||||
if promoted.Error != nil {
|
||||
return promoted.Error
|
||||
}
|
||||
if promoted.RowsAffected == 0 {
|
||||
return domainUser.ErrInvalidTransfer
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ domainUser.Repository = &UserRepository{}
|
||||
Reference in new issue
Block a user