package db import ( "context" "time" "git.haelnorr.com/h/oslstats/internal/roles" "github.com/pkg/errors" "github.com/uptrace/bun" ) 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 } // AssignRole grants a role to a user func AssignRole(ctx context.Context, tx bun.Tx, userID, roleID int, grantedBy *int) error { if userID <= 0 { return errors.New("userID must be positive") } if roleID <= 0 { 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) if err != nil { return errors.Wrap(err, "tx.ExecContext") } return nil } // RevokeRole removes a role from a user func RevokeRole(ctx context.Context, tx bun.Tx, userID, roleID int) error { if userID <= 0 { return errors.New("userID must be positive") } if roleID <= 0 { 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) if err != nil { return errors.Wrap(err, "tx.ExecContext") } return nil } // HasRole checks if a user has a specific role func HasRole(ctx context.Context, tx bun.Tx, userID int, roleName roles.Role) (bool, error) { if userID <= 0 { return false, errors.New("userID must be positive") } 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) if err != nil { return false, errors.Wrap(err, "tx.NewSelect") } return count > 0, nil }