Files
oslstats/internal/db/role.go
2026-02-14 14:54:06 +11:00

193 lines
4.9 KiB
Go

package db
import (
"context"
"time"
"git.haelnorr.com/h/oslstats/internal/roles"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
type Role struct {
bun.BaseModel `bun:"table:roles,alias:r"`
ID int `bun:"id,pk,autoincrement"`
Name roles.Role `bun:"name,unique,notnull"`
DisplayName string `bun:"display_name,notnull"`
Description string `bun:"description"`
IsSystem bool `bun:"is_system,default:false"`
CreatedAt int64 `bun:"created_at,notnull"`
UpdatedAt *int64 `bun:"updated_at"`
// Relations (loaded on demand)
Users []User `bun:"m2m:user_roles,join:Role=User"`
Permissions []Permission `bun:"m2m:role_permissions,join:Role=Permission"`
}
type RolePermission struct {
RoleID int `bun:",pk"`
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
PermissionID int `bun:",pk"`
Permission *Permission `bun:"rel:belongs-to,join:permission_id=id"`
}
func (r Role) isSystem() bool {
return r.IsSystem
}
// GetRoleByName queries the database for a role matching the given name
// Returns nil, nil if no role is found
func GetRoleByName(ctx context.Context, tx bun.Tx, name roles.Role) (*Role, error) {
if name == "" {
return nil, errors.New("name cannot be empty")
}
return GetByField[Role](tx, "name", name).Get(ctx)
}
// GetRoleByID queries the database for a role matching the given ID
// Returns nil, nil if no role is found
func GetRoleByID(ctx context.Context, tx bun.Tx, id int) (*Role, error) {
return GetByID[Role](tx, id).Get(ctx)
}
// GetRoleWithPermissions loads a role and all its permissions
func GetRoleWithPermissions(ctx context.Context, tx bun.Tx, id int) (*Role, error) {
return GetByID[Role](tx, id).Relation("Permissions").Get(ctx)
}
// ListAllRoles returns all roles
func ListAllRoles(ctx context.Context, tx bun.Tx) ([]*Role, error) {
return GetList[Role](tx).GetAll(ctx)
}
// GetRoles returns a paginated list of roles
func GetRoles(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*List[Role], error) {
defaults := &PageOpts{
Page: 1,
PerPage: 25,
Order: bun.OrderAsc,
OrderBy: "display_name",
}
return GetList[Role](tx).GetPaged(ctx, pageOpts, defaults)
}
// CreateRole creates a new role
func CreateRole(ctx context.Context, tx bun.Tx, role *Role) error {
if role == nil {
return errors.New("role cannot be nil")
}
role.CreatedAt = time.Now().Unix()
err := Insert(tx, role).
Returning("id").
Exec(ctx)
if err != nil {
return errors.Wrap(err, "db.Insert")
}
return nil
}
// UpdateRole updates an existing role
func UpdateRole(ctx context.Context, tx bun.Tx, role *Role) error {
if role == nil {
return errors.New("role cannot be nil")
}
if role.ID <= 0 {
return errors.New("role id must be positive")
}
err := Update(tx, role).
WherePK().
Exec(ctx)
if err != nil {
return errors.Wrap(err, "db.Update")
}
return nil
}
// DeleteRole deletes a role (checks IsSystem protection)
// Also cleans up join table entries in role_permissions and user_roles
func DeleteRole(ctx context.Context, tx bun.Tx, id int) error {
if id <= 0 {
return errors.New("id must be positive")
}
// First check if role exists and is not system
role, err := GetRoleByID(ctx, tx, id)
if err != nil {
return errors.Wrap(err, "GetRoleByID")
}
if role == nil {
return errors.New("role not found")
}
if role.IsSystem {
return errors.New("cannot delete system roles")
}
// Delete role_permissions entries
_, err = tx.NewDelete().
Model((*RolePermission)(nil)).
Where("role_id = ?", id).
Exec(ctx)
if err != nil {
return errors.Wrap(err, "delete role_permissions")
}
// Delete user_roles entries
_, err = tx.NewDelete().
Model((*UserRole)(nil)).
Where("role_id = ?", id).
Exec(ctx)
if err != nil {
return errors.Wrap(err, "delete user_roles")
}
// Finally delete the role
return DeleteWithProtection[Role](ctx, tx, id)
}
// AddPermissionToRole grants a permission to a role
func AddPermissionToRole(ctx context.Context, tx bun.Tx, roleID, permissionID int) error {
if roleID <= 0 {
return errors.New("roleID must be positive")
}
if permissionID <= 0 {
return errors.New("permissionID must be positive")
}
rolePerm := &RolePermission{
RoleID: roleID,
PermissionID: permissionID,
}
err := Insert(tx, rolePerm).
ConflictNothing("role_id", "permission_id").
Exec(ctx)
if err != nil {
return errors.Wrap(err, "db.Insert")
}
return nil
}
// RemovePermissionFromRole revokes a permission from a role
func RemovePermissionFromRole(ctx context.Context, tx bun.Tx, roleID, permissionID int) error {
if roleID <= 0 {
return errors.New("roleID must be positive")
}
if permissionID <= 0 {
return errors.New("permissionID must be positive")
}
err := DeleteItem[RolePermission](tx).
Where("role_id = ?", roleID).
Where("permission_id = ?", permissionID).
Delete(ctx)
if err != nil {
return errors.Wrap(err, "DeleteItem")
}
return nil
}