feat: /v1/role list, get, create, update and delete. Reassign members on delete. Write-time validation (ValidateGrants, duplicates, typed keys against real content types). Name uniqueness (role_name_taken), role_locked, role_has_members, role_conflict on version mismatch. No-escalation: nobody grants a permission they don't hold, and roles.edit_owner_only guards the Owner role. Activity events for role changes. Typed-key exposure in /auth/me. Cleanup of typed rows when a content type is deleted. | Backend only. Will carry the deferred ListGrants decision.
This commit is contained in:
1 parent
a043f7f41a
commit
094df63a7f
51 files changed
+4822
-115
No files matched your search
@@ -1,11 +1,14 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type RoleRepository struct {
|
||||
@@ -37,9 +40,17 @@ func (r *RoleRepository) List() ([]role.Role, error) {
|
||||
if err := r.db.Order("id asc").Find(&found).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []entity.RolePermission
|
||||
if err := r.db.Order("id asc").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rowsByRole := make(map[uint32][]entity.RolePermission, len(found))
|
||||
for _, row := range rows {
|
||||
rowsByRole[row.RoleID] = append(rowsByRole[row.RoleID], row)
|
||||
}
|
||||
roles := make([]role.Role, 0, len(found))
|
||||
for _, row := range found {
|
||||
roles = append(roles, mappers.RoleToDomain(row, nil))
|
||||
roles = append(roles, mappers.RoleToDomain(row, rowsByRole[row.ID]))
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
@@ -60,18 +71,28 @@ func (r *RoleRepository) GetByName(name string) (role.Role, error) {
|
||||
return r.withGrants(found)
|
||||
}
|
||||
|
||||
func (r *RoleRepository) ListGrants(roleID uint32) ([]role.Grant, error) {
|
||||
rows, err := r.grantRows(roleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func ensureNameUnused(tx *gorm.DB, name string, excludedID uint32) error {
|
||||
var taken int64
|
||||
query := tx.Model(&entity.Role{}).Where("LOWER(name) = LOWER(?)", strings.TrimSpace(name))
|
||||
if excludedID != 0 {
|
||||
query = query.Where("id <> ?", excludedID)
|
||||
}
|
||||
return mappers.GrantsToDomain(rows), nil
|
||||
if err := query.Count(&taken).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if taken > 0 {
|
||||
return role.ErrRoleNameTaken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RoleRepository) Create(created role.Role) (role.Role, error) {
|
||||
model := mappers.RoleToModel(created)
|
||||
model.ID = 0
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := ensureNameUnused(tx, created.Name, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,9 +113,12 @@ func (r *RoleRepository) Save(saved role.Role) (role.Role, error) {
|
||||
if err := tx.First(&entity.Role{}, saved.ID).Error; err != nil {
|
||||
return mapNotFoundErr(err, role.ErrRoleNotFound)
|
||||
}
|
||||
if err := ensureNameUnused(tx, saved.Name, saved.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
update := tx.Model(&entity.Role{}).
|
||||
Where("id = ? AND version = ?", saved.ID, saved.Version).
|
||||
Updates(map[string]any{"name": saved.Name, "description": saved.Description, "version": saved.Version + 1})
|
||||
Updates(map[string]any{"name": saved.Name, "description": saved.Description, "version": saved.Version + 1, "updated_at": mappers.RoleToModel(saved).EditedAt, "updated_by": saved.UpdatedBy})
|
||||
if update.Error != nil {
|
||||
return update.Error
|
||||
}
|
||||
@@ -116,4 +140,66 @@ func (r *RoleRepository) Save(saved role.Role) (role.Role, error) {
|
||||
return r.Get(saved.ID)
|
||||
}
|
||||
|
||||
func (r *RoleRepository) Delete(id uint32) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) 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 {
|
||||
return err
|
||||
}
|
||||
if members > 0 {
|
||||
return role.ErrRoleHasMembers
|
||||
}
|
||||
if err := tx.Where("role_id = ?", id).Delete(&entity.RolePermission{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&entity.Role{}, id).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 {
|
||||
return 0, err
|
||||
}
|
||||
if targets == 0 {
|
||||
return 0, role.ErrInvalidReassign
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (r *RoleRepository) RemoveTypedGrants(typeKey string) (int, error) {
|
||||
affected := 0
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var roleIDs []uint32
|
||||
doomed := role.TypedKeys(typeKey)
|
||||
if err := tx.Model(&entity.RolePermission{}).Where("permission_key IN ?", doomed).Distinct().Pluck("role_id", &roleIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(roleIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := tx.Where("permission_key IN ?", doomed).Delete(&entity.RolePermission{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
affected = len(roleIDs)
|
||||
return tx.Model(&entity.Role{}).Where("id IN ?", roleIDs).UpdateColumn("version", gorm.Expr("version + 1")).Error
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
var _ role.Repository = &RoleRepository{}
|
||||
Reference in new issue
Block a user