feat: Users API, ownership transfer and removal of the legacy role string

This commit is contained in:
Florian Sylvain committed 2026-10-27 03:08:15 +01:00
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
})
}