250 lines
7.1 KiB
Go
250 lines
7.1 KiB
Go
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 {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewRoleRepository(db *gorm.DB) *RoleRepository {
|
|
return &RoleRepository{db}
|
|
}
|
|
|
|
func (r *RoleRepository) grantRows(roleID uint32) ([]entity.RolePermission, error) {
|
|
var rows []entity.RolePermission
|
|
if err := r.db.Where("role_id = ?", roleID).Order("id asc").Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (r *RoleRepository) withGrants(found entity.Role) (role.Role, error) {
|
|
rows, err := r.grantRows(found.ID)
|
|
if err != nil {
|
|
return role.Role{}, err
|
|
}
|
|
return mappers.RoleToDomain(found, rows), nil
|
|
}
|
|
|
|
func (r *RoleRepository) List() ([]role.Role, error) {
|
|
var found []entity.Role
|
|
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, rowsByRole[row.ID]))
|
|
}
|
|
return roles, nil
|
|
}
|
|
|
|
func (r *RoleRepository) Get(id uint32) (role.Role, error) {
|
|
var found entity.Role
|
|
if err := r.db.First(&found, id).Error; err != nil {
|
|
return role.Role{}, mapNotFoundErr(err, role.ErrRoleNotFound)
|
|
}
|
|
return r.withGrants(found)
|
|
}
|
|
|
|
func (r *RoleRepository) GetByName(name string) (role.Role, error) {
|
|
var found entity.Role
|
|
if err := r.db.Where("name = ?", name).Order("built_in DESC, id ASC").First(&found).Error; err != nil {
|
|
return role.Role{}, mapNotFoundErr(err, role.ErrRoleNotFound)
|
|
}
|
|
return r.withGrants(found)
|
|
}
|
|
|
|
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)
|
|
}
|
|
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
|
|
}
|
|
rows := mappers.GrantsToModel(model.ID, created.Grants)
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
return tx.Create(&rows).Error
|
|
})
|
|
if err != nil {
|
|
return role.Role{}, err
|
|
}
|
|
return r.withGrants(model)
|
|
}
|
|
|
|
func (r *RoleRepository) Save(saved role.Role) (role.Role, error) {
|
|
err := r.db.Transaction(func(tx *gorm.DB) 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, "updated_at": mappers.RoleToModel(saved).EditedAt, "updated_by": saved.UpdatedBy})
|
|
if update.Error != nil {
|
|
return update.Error
|
|
}
|
|
if update.RowsAffected == 0 {
|
|
return role.ErrRoleConflict
|
|
}
|
|
if err := tx.Where("role_id = ?", saved.ID).Delete(&entity.RolePermission{}).Error; err != nil {
|
|
return err
|
|
}
|
|
rows := mappers.GrantsToModel(saved.ID, saved.Grants)
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
return tx.Create(&rows).Error
|
|
})
|
|
if err != nil {
|
|
return role.Role{}, err
|
|
}
|
|
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)
|
|
}
|
|
members, err := countMembers(tx, id)
|
|
if 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 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 err := tx.Model(&entity.Invite{}).Where("role_id = ? AND accepted_at IS NULL", id).Count(&invites).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
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) {
|
|
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{}
|