fixed relationship issues
This commit is contained in:
@@ -28,7 +28,11 @@ type AuditLog struct {
|
||||
User *User `bun:"rel:belongs-to,join:user_id=id"`
|
||||
}
|
||||
|
||||
// TODO: add AuditLogs to match list style with PageOpts
|
||||
type AuditLogs struct {
|
||||
AuditLogs []*AuditLog
|
||||
Total int
|
||||
PageOpts PageOpts
|
||||
}
|
||||
|
||||
// CreateAuditLog creates a new audit log entry
|
||||
func CreateAuditLog(ctx context.Context, tx bun.Tx, log *AuditLog) error {
|
||||
@@ -54,12 +58,12 @@ type AuditLogFilters struct {
|
||||
}
|
||||
|
||||
// GetAuditLogs retrieves audit logs with optional filters and pagination
|
||||
// TODO: change this to use db.PageOpts
|
||||
func GetAuditLogs(ctx context.Context, tx bun.Tx, limit, offset int, filters *AuditLogFilters) ([]*AuditLog, int, error) {
|
||||
func GetAuditLogs(ctx context.Context, tx bun.Tx, pageOpts *PageOpts, filters *AuditLogFilters) (*AuditLogs, error) {
|
||||
pageOpts = setDefaultPageOpts(pageOpts, 1, 50, bun.OrderDesc, "created_at")
|
||||
query := tx.NewSelect().
|
||||
Model((*AuditLog)(nil)).
|
||||
Relation("User").
|
||||
Order("created_at DESC")
|
||||
OrderBy(pageOpts.OrderBy, pageOpts.Order)
|
||||
|
||||
// Apply filters if provided
|
||||
if filters != nil {
|
||||
@@ -80,48 +84,52 @@ func GetAuditLogs(ctx context.Context, tx bun.Tx, limit, offset int, filters *Au
|
||||
// Get total count
|
||||
total, err := query.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "query.Count")
|
||||
return nil, errors.Wrap(err, "query.Count")
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
var logs []*AuditLog
|
||||
logs := new([]*AuditLog)
|
||||
err = query.
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Offset(pageOpts.PerPage*(pageOpts.Page-1)).
|
||||
Limit(pageOpts.PerPage).
|
||||
Scan(ctx, &logs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, 0, errors.Wrap(err, "query.Scan")
|
||||
return nil, errors.Wrap(err, "query.Scan")
|
||||
}
|
||||
|
||||
return logs, total, nil
|
||||
list := &AuditLogs{
|
||||
AuditLogs: *logs,
|
||||
Total: total,
|
||||
PageOpts: *pageOpts,
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetAuditLogsByUser retrieves audit logs for a specific user
|
||||
// TODO: change this to use db.PageOpts
|
||||
func GetAuditLogsByUser(ctx context.Context, tx bun.Tx, userID int, limit, offset int) ([]*AuditLog, int, error) {
|
||||
func GetAuditLogsByUser(ctx context.Context, tx bun.Tx, userID int, pageOpts *PageOpts) (*AuditLogs, error) {
|
||||
if userID <= 0 {
|
||||
return nil, 0, errors.New("userID must be positive")
|
||||
return nil, errors.New("userID must be positive")
|
||||
}
|
||||
|
||||
filters := &AuditLogFilters{
|
||||
UserID: &userID,
|
||||
}
|
||||
|
||||
return GetAuditLogs(ctx, tx, limit, offset, filters)
|
||||
return GetAuditLogs(ctx, tx, pageOpts, filters)
|
||||
}
|
||||
|
||||
// GetAuditLogsByAction retrieves audit logs for a specific action
|
||||
// TODO: change this to use db.PageOpts
|
||||
func GetAuditLogsByAction(ctx context.Context, tx bun.Tx, action string, limit, offset int) ([]*AuditLog, int, error) {
|
||||
func GetAuditLogsByAction(ctx context.Context, tx bun.Tx, action string, pageOpts *PageOpts) (*AuditLogs, error) {
|
||||
if action == "" {
|
||||
return nil, 0, errors.New("action cannot be empty")
|
||||
return nil, errors.New("action cannot be empty")
|
||||
}
|
||||
|
||||
filters := &AuditLogFilters{
|
||||
Action: &action,
|
||||
}
|
||||
|
||||
return GetAuditLogs(ctx, tx, limit, offset, filters)
|
||||
return GetAuditLogs(ctx, tx, pageOpts, filters)
|
||||
}
|
||||
|
||||
// CleanupOldAuditLogs deletes audit logs older than the specified timestamp
|
||||
|
||||
@@ -15,6 +15,25 @@ type OrderOpts struct {
|
||||
Label string
|
||||
}
|
||||
|
||||
func setDefaultPageOpts(p *PageOpts, page, perpage int, order bun.Order, orderby string) *PageOpts {
|
||||
if p == nil {
|
||||
p = new(PageOpts)
|
||||
}
|
||||
if p.Page == 0 {
|
||||
p.Page = page
|
||||
}
|
||||
if p.PerPage == 0 {
|
||||
p.PerPage = perpage
|
||||
}
|
||||
if p.Order == "" {
|
||||
p.Order = order
|
||||
}
|
||||
if p.OrderBy == "" {
|
||||
p.OrderBy = orderby
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// TotalPages calculates the total number of pages
|
||||
func (p *PageOpts) TotalPages(total int) int {
|
||||
if p.PerPage == 0 {
|
||||
|
||||
@@ -20,6 +20,8 @@ type Permission struct {
|
||||
Action string `bun:"action,notnull"`
|
||||
IsSystem bool `bun:"is_system,default:false"`
|
||||
CreatedAt int64 `bun:"created_at,notnull"`
|
||||
|
||||
Roles []Role `bun:"m2m:role_permissions,join:Permission=Role"`
|
||||
}
|
||||
|
||||
// GetPermissionByName queries the database for a permission matching the given name
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.haelnorr.com/h/oslstats/internal/roles"
|
||||
"github.com/pkg/errors"
|
||||
@@ -18,10 +19,18 @@ type Role struct {
|
||||
Description string `bun:"description"`
|
||||
IsSystem bool `bun:"is_system,default:false"`
|
||||
CreatedAt int64 `bun:"created_at,notnull"`
|
||||
UpdatedAt int64 `bun:"updated_at,notnull"`
|
||||
UpdatedAt *int64 `bun:"updated_at"`
|
||||
|
||||
// Relations (loaded on demand)
|
||||
Permissions []*Permission `bun:"m2m:role_permissions,join:Role=Permission"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// GetRoleByName queries the database for a role matching the given name
|
||||
@@ -99,6 +108,7 @@ 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 := tx.NewInsert().
|
||||
Model(role).
|
||||
@@ -160,23 +170,23 @@ func DeleteRole(ctx context.Context, tx bun.Tx, id int) error {
|
||||
}
|
||||
|
||||
// AddPermissionToRole grants a permission to a role
|
||||
func AddPermissionToRole(ctx context.Context, tx bun.Tx, roleID, permissionID int, createdAt int64) error {
|
||||
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")
|
||||
}
|
||||
|
||||
// TODO: use proper m2m table
|
||||
// also make createdAt automatic in table so not required as input here
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO role_permissions (role_id, permission_id, created_at)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (role_id, permission_id) DO NOTHING
|
||||
`, roleID, permissionID, createdAt)
|
||||
rolePerm := &RolePermission{
|
||||
RoleID: roleID,
|
||||
PermissionID: permissionID,
|
||||
}
|
||||
_, err := tx.NewInsert().
|
||||
Model(rolePerm).
|
||||
On("CONFLICT (role_id, permission_id) DO NOTHING").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.ExecContext")
|
||||
return errors.Wrap(err, "tx.NewInsert")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -191,13 +201,13 @@ func RemovePermissionFromRole(ctx context.Context, tx bun.Tx, roleID, permission
|
||||
return errors.New("permissionID must be positive")
|
||||
}
|
||||
|
||||
// TODO: use proper m2m table
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM role_permissions
|
||||
WHERE role_id = $1 AND permission_id = $2
|
||||
`, roleID, permissionID)
|
||||
_, err := tx.NewDelete().
|
||||
Model((*RolePermission)(nil)).
|
||||
Where("role_id = ?", roleID).
|
||||
Where("permission_id = ?", permissionID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.ExecContext")
|
||||
return errors.Wrap(err, "tx.NewDelete")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -23,7 +23,7 @@ type Season struct {
|
||||
}
|
||||
|
||||
type SeasonList struct {
|
||||
Seasons []Season
|
||||
Seasons []*Season
|
||||
Total int
|
||||
PageOpts PageOpts
|
||||
}
|
||||
@@ -50,24 +50,10 @@ func NewSeason(ctx context.Context, tx bun.Tx, name, shortname string, start tim
|
||||
}
|
||||
|
||||
func ListSeasons(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*SeasonList, error) {
|
||||
if pageOpts == nil {
|
||||
pageOpts = &PageOpts{}
|
||||
}
|
||||
if pageOpts.Page == 0 {
|
||||
pageOpts.Page = 1
|
||||
}
|
||||
if pageOpts.PerPage == 0 {
|
||||
pageOpts.PerPage = 10
|
||||
}
|
||||
if pageOpts.Order == "" {
|
||||
pageOpts.Order = bun.OrderDesc
|
||||
}
|
||||
if pageOpts.OrderBy == "" {
|
||||
pageOpts.OrderBy = "start_date"
|
||||
}
|
||||
seasons := []Season{}
|
||||
pageOpts = setDefaultPageOpts(pageOpts, 1, 10, bun.OrderDesc, "start_date")
|
||||
seasons := new([]*Season)
|
||||
err := tx.NewSelect().
|
||||
Model(&seasons).
|
||||
Model(seasons).
|
||||
OrderBy(pageOpts.OrderBy, pageOpts.Order).
|
||||
Offset(pageOpts.PerPage * (pageOpts.Page - 1)).
|
||||
Limit(pageOpts.PerPage).
|
||||
@@ -76,13 +62,13 @@ func ListSeasons(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*SeasonLis
|
||||
return nil, errors.Wrap(err, "tx.NewSelect")
|
||||
}
|
||||
total, err := tx.NewSelect().
|
||||
Model(&seasons).
|
||||
Model(seasons).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.NewSelect")
|
||||
}
|
||||
sl := &SeasonList{
|
||||
Seasons: seasons,
|
||||
Seasons: *seasons,
|
||||
Total: total,
|
||||
PageOpts: *pageOpts,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ type User struct {
|
||||
Username string `bun:"username,unique"` // Username (unique)
|
||||
CreatedAt int64 `bun:"created_at"` // Epoch timestamp when the user was added to the database
|
||||
DiscordID string `bun:"discord_id,unique"`
|
||||
|
||||
Roles []*Role `bun:"m2m:user_roles,join:User=Role"`
|
||||
}
|
||||
|
||||
type Users struct {
|
||||
@@ -124,28 +126,35 @@ func IsUsernameUnique(ctx context.Context, tx bun.Tx, username string) (bool, er
|
||||
return count == 0, nil
|
||||
}
|
||||
|
||||
// GetRoles loads and returns all roles for this user
|
||||
// GetRoles loads all the roles for this user
|
||||
func (u *User) GetRoles(ctx context.Context, tx bun.Tx) ([]*Role, error) {
|
||||
if u == nil {
|
||||
return nil, errors.New("user cannot be nil")
|
||||
}
|
||||
return GetUserRoles(ctx, tx, u.ID)
|
||||
|
||||
err := tx.NewSelect().
|
||||
Model(u).
|
||||
Relation("Roles").
|
||||
Where("id = ?", u.ID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.NewSelect")
|
||||
}
|
||||
return u.Roles, nil
|
||||
}
|
||||
|
||||
// GetPermissions loads and returns all permissions for this user (via roles)
|
||||
// GetPermissions loads and returns all permissions for this user
|
||||
func (u *User) GetPermissions(ctx context.Context, tx bun.Tx) ([]*Permission, error) {
|
||||
if u == nil {
|
||||
return nil, errors.New("user cannot be nil")
|
||||
}
|
||||
|
||||
// TODO: use proper m2m tables and relations instead of join
|
||||
var permissions []*Permission
|
||||
err := tx.NewSelect().
|
||||
Model(&permissions).
|
||||
Join("JOIN role_permissions AS rp ON rp.permission_id = p.id").
|
||||
Join("JOIN role_permissions AS rp on rp.permission_id = p.id").
|
||||
Join("JOIN user_roles AS ur ON ur.role_id = rp.role_id").
|
||||
Where("ur.user_id = ?", u.ID).
|
||||
Where("ur.expires_at IS NULL OR ur.expires_at > ?", time.Now().Unix()).
|
||||
Scan(ctx)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, errors.Wrap(err, "tx.NewSelect")
|
||||
@@ -192,22 +201,8 @@ func (u *User) IsAdmin(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
}
|
||||
|
||||
func GetUsers(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*Users, error) {
|
||||
if pageOpts == nil {
|
||||
pageOpts = &PageOpts{}
|
||||
}
|
||||
if pageOpts.Page == 0 {
|
||||
pageOpts.Page = 1
|
||||
}
|
||||
if pageOpts.PerPage == 0 {
|
||||
pageOpts.PerPage = 50
|
||||
}
|
||||
if pageOpts.Order == "" {
|
||||
pageOpts.Order = bun.OrderAsc
|
||||
}
|
||||
if pageOpts.OrderBy == "" {
|
||||
pageOpts.OrderBy = "id"
|
||||
}
|
||||
users := []*User{}
|
||||
pageOpts = setDefaultPageOpts(pageOpts, 1, 50, bun.OrderAsc, "id")
|
||||
users := new([]*User)
|
||||
err := tx.NewSelect().
|
||||
Model(users).
|
||||
OrderBy(pageOpts.OrderBy, pageOpts.Order).
|
||||
@@ -224,7 +219,7 @@ func GetUsers(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*Users, error
|
||||
return nil, errors.Wrap(err, "tx.NewSelect")
|
||||
}
|
||||
list := &Users{
|
||||
Users: users,
|
||||
Users: *users,
|
||||
Total: total,
|
||||
PageOpts: *pageOpts,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.haelnorr.com/h/oslstats/internal/roles"
|
||||
"github.com/pkg/errors"
|
||||
@@ -10,42 +9,14 @@ import (
|
||||
)
|
||||
|
||||
type UserRole struct {
|
||||
bun.BaseModel `bun:"table:user_roles,alias:ur"`
|
||||
|
||||
ID int `bun:"id,pk,autoincrement"`
|
||||
UserID int `bun:"user_id,notnull"`
|
||||
RoleID int `bun:"role_id,notnull"`
|
||||
GrantedBy *int `bun:"granted_by"`
|
||||
GrantedAt int64 `bun:"granted_at,notnull"` // TODO: default now
|
||||
ExpiresAt *int64 `bun:"expires_at"`
|
||||
|
||||
// Relations
|
||||
User *User `bun:"rel:belongs-to,join:user_id=id"`
|
||||
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
|
||||
}
|
||||
|
||||
// GetUserRoles loads all roles for a given user
|
||||
func GetUserRoles(ctx context.Context, tx bun.Tx, userID int) ([]*Role, error) {
|
||||
if userID <= 0 {
|
||||
return nil, errors.New("userID must be positive")
|
||||
}
|
||||
|
||||
var roles []*Role
|
||||
err := tx.NewSelect().
|
||||
Model(&roles).
|
||||
// TODO: why are we joining? can we do relation?
|
||||
Join("JOIN user_roles AS ur ON ur.role_id = r.id").
|
||||
Where("ur.user_id = ?", userID).
|
||||
Where("ur.expires_at IS NULL OR ur.expires_at > ?", time.Now().Unix()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.NewSelect")
|
||||
}
|
||||
return roles, nil
|
||||
UserID int `bun:",pk"`
|
||||
User *User `bun:"rel:belongs-to,join:user_id=id"`
|
||||
RoleID int `bun:",pk"`
|
||||
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
|
||||
}
|
||||
|
||||
// AssignRole grants a role to a user
|
||||
func AssignRole(ctx context.Context, tx bun.Tx, userID, roleID int, grantedBy *int) error {
|
||||
func AssignRole(ctx context.Context, tx bun.Tx, userID, roleID int) error {
|
||||
if userID <= 0 {
|
||||
return errors.New("userID must be positive")
|
||||
}
|
||||
@@ -53,16 +24,16 @@ func AssignRole(ctx context.Context, tx bun.Tx, userID, roleID int, grantedBy *i
|
||||
return errors.New("roleID must be positive")
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
// TODO: use proper m2m table instead of raw SQL
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO user_roles (user_id, role_id, granted_by, granted_at)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING
|
||||
`, userID, roleID, grantedBy, now)
|
||||
userRole := &UserRole{
|
||||
UserID: userID,
|
||||
RoleID: roleID,
|
||||
}
|
||||
_, err := tx.NewInsert().
|
||||
Model(userRole).
|
||||
On("CONFLICT (user_id, role_id) DO NOTHING").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.ExecContext")
|
||||
return errors.Wrap(err, "tx.NewInsert")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -77,13 +48,13 @@ func RevokeRole(ctx context.Context, tx bun.Tx, userID, roleID int) error {
|
||||
return errors.New("roleID must be positive")
|
||||
}
|
||||
|
||||
// TODO: use proper m2m table instead of raw sql
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM user_roles
|
||||
WHERE user_id = $1 AND role_id = $2
|
||||
`, userID, roleID)
|
||||
_, err := tx.NewDelete().
|
||||
Model((*UserRole)(nil)).
|
||||
Where("user_id = ?", userID).
|
||||
Where("role_id = ?", roleID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.ExecContext")
|
||||
return errors.Wrap(err, "tx.NewDelete")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -97,18 +68,19 @@ func HasRole(ctx context.Context, tx bun.Tx, userID int, roleName roles.Role) (b
|
||||
if roleName == "" {
|
||||
return false, errors.New("roleName cannot be empty")
|
||||
}
|
||||
|
||||
// TODO: use proper m2m table instead of TableExpr and Join?
|
||||
count, err := tx.NewSelect().
|
||||
TableExpr("user_roles AS ur").
|
||||
Join("JOIN roles AS r ON r.id = ur.role_id").
|
||||
Where("ur.user_id = ?", userID).
|
||||
Where("r.name = ?", roleName).
|
||||
Where("ur.expires_at IS NULL OR ur.expires_at > ?", time.Now().Unix()).
|
||||
Count(ctx)
|
||||
user := new(User)
|
||||
err := tx.NewSelect().
|
||||
Model(user).
|
||||
Relation("Roles").
|
||||
Where("u.id = ? ", userID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "tx.NewSelect")
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
for _, role := range user.Roles {
|
||||
if role.Name == roleName {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user