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 }