mirror of
https://github.com/goharbor/harbor
synced 2025-05-19 10:31:12 +00:00
Add usergroup search API (#15483)
Fixes #15450 Add paging function to usergroup list/search API Fix some 500 error when adding LDAP user/group to project member Signed-off-by: stonezdj <stonezdj@gmail.com>
This commit is contained in:
parent
ff617950b7
commit
6b8c5c9edd
@ -2697,6 +2697,8 @@ paths:
|
||||
- usergroup
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/page'
|
||||
- $ref: '#/parameters/pageSize'
|
||||
- name: ldap_group_dn
|
||||
in: query
|
||||
type: string
|
||||
@ -2709,6 +2711,13 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/UserGroup'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of available items
|
||||
type: integer
|
||||
Link:
|
||||
description: Link to previous page and next page
|
||||
type: string
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
@ -2744,6 +2753,41 @@ paths:
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/usergroups/search:
|
||||
get:
|
||||
summary: Search groups by groupname
|
||||
description: |
|
||||
This endpoint is to search groups by group name. It's open for all authenticated requests.
|
||||
tags:
|
||||
- usergroup
|
||||
operationId: searchUserGroups
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/page'
|
||||
- $ref: '#/parameters/pageSize'
|
||||
- name: groupname
|
||||
in: query
|
||||
type: string
|
||||
required: true
|
||||
description: Group name for filtering results.
|
||||
responses:
|
||||
'200':
|
||||
description: Search groups successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/UserGroupSearchItem'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of available items
|
||||
type: integer
|
||||
Link:
|
||||
description: Link to previous page and next page
|
||||
type: string
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
'/usergroups/{group_id}':
|
||||
get:
|
||||
summary: Get user group information
|
||||
@ -7689,6 +7733,18 @@ definitions:
|
||||
ldap_group_dn:
|
||||
type: string
|
||||
description: The DN of the LDAP group if group type is 1 (LDAP group).
|
||||
UserGroupSearchItem:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: The ID of the user group
|
||||
group_name:
|
||||
type: string
|
||||
description: The name of the user group
|
||||
group_type:
|
||||
type: integer
|
||||
description: 'The group type, 1 for LDAP group, 2 for HTTP group.'
|
||||
SupportedWebhookEventTypes:
|
||||
type: object
|
||||
description: Supportted webhook event types and notify types.
|
||||
|
@ -26,7 +26,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/project"
|
||||
"github.com/goharbor/harbor/src/pkg/user"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup"
|
||||
ugModel "github.com/goharbor/harbor/src/pkg/usergroup/model"
|
||||
)
|
||||
|
||||
// Controller defines the operation related to project member
|
||||
@ -151,14 +150,26 @@ func (c *controller) Create(ctx context.Context, projectNameOrID interface{}, re
|
||||
member.EntityID = userID
|
||||
} else if len(req.MemberGroup.LdapGroupDN) > 0 {
|
||||
req.MemberGroup.GroupType = common.LDAPGroupType
|
||||
// If groupname provided, use the provided groupname to name this group
|
||||
groupID, err := auth.SearchAndOnBoardGroup(req.MemberGroup.LdapGroupDN, req.MemberGroup.GroupName)
|
||||
// if the ldap group dn already exist
|
||||
ugs, err := usergroup.Mgr.List(ctx, q.New(q.KeyWords{"LdapGroupDN": req.MemberGroup.LdapGroupDN, "GroupType": req.MemberGroup.GroupType}))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
member.EntityID = groupID
|
||||
} else if len(req.MemberGroup.GroupName) > 0 && req.MemberGroup.GroupType == common.HTTPGroupType || req.MemberGroup.GroupType == common.OIDCGroupType {
|
||||
ugs, err := usergroup.Mgr.List(ctx, ugModel.UserGroup{GroupName: req.MemberGroup.GroupName, GroupType: req.MemberGroup.GroupType})
|
||||
if len(ugs) > 0 {
|
||||
member.EntityID = ugs[0].ID
|
||||
member.EntityType = common.GroupMember
|
||||
} else {
|
||||
// If groupname provided, use the provided groupname to name this group
|
||||
groupID, err := auth.SearchAndOnBoardGroup(req.MemberGroup.LdapGroupDN, req.MemberGroup.GroupName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
member.EntityID = groupID
|
||||
}
|
||||
|
||||
} else if len(req.MemberGroup.GroupName) > 0 {
|
||||
// all group type can be added to project member by name
|
||||
ugs, err := usergroup.Mgr.List(ctx, q.New(q.KeyWords{"GroupName": req.MemberGroup.GroupName, "GroupType": req.MemberGroup.GroupType}))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/ldap"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup/model"
|
||||
@ -44,7 +45,9 @@ type Controller interface {
|
||||
// Populate populate user group and get the user group's id
|
||||
Populate(ctx context.Context, userGroups []model.UserGroup) ([]int, error)
|
||||
// List list user groups
|
||||
List(ctx context.Context, userGroup model.UserGroup) ([]*model.UserGroup, error)
|
||||
List(ctx context.Context, q *q.Query) ([]*model.UserGroup, error)
|
||||
// Count user group count
|
||||
Count(ctx context.Context, q *q.Query) (int64, error)
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
@ -55,8 +58,8 @@ func newController() Controller {
|
||||
return &controller{mgr: usergroup.Mgr}
|
||||
}
|
||||
|
||||
func (c *controller) List(ctx context.Context, userGroup model.UserGroup) ([]*model.UserGroup, error) {
|
||||
return c.mgr.List(ctx, userGroup)
|
||||
func (c *controller) List(ctx context.Context, query *q.Query) ([]*model.UserGroup, error) {
|
||||
return c.mgr.List(ctx, query)
|
||||
}
|
||||
|
||||
func (c *controller) Populate(ctx context.Context, userGroups []model.UserGroup) ([]int, error) {
|
||||
@ -72,7 +75,7 @@ func (c *controller) Delete(ctx context.Context, id int) error {
|
||||
}
|
||||
|
||||
func (c *controller) Update(ctx context.Context, id int, groupName string) error {
|
||||
ug, err := c.mgr.List(ctx, model.UserGroup{ID: id})
|
||||
ug, err := c.mgr.List(ctx, q.New(q.KeyWords{"ID": id}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -109,3 +112,7 @@ func (c *controller) Create(ctx context.Context, group model.UserGroup) (int, er
|
||||
func (c *controller) Get(ctx context.Context, id int) (*model.UserGroup, error) {
|
||||
return c.mgr.Get(ctx, id)
|
||||
}
|
||||
|
||||
func (c *controller) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
return c.mgr.Count(ctx, query)
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ func SearchAndOnBoardUser(username string) (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
if user == nil {
|
||||
return 0, ErrorUserNotExist
|
||||
return 0, libErrors.NotFoundError(nil).WithMessage(fmt.Sprintf("user %s is not found", username))
|
||||
}
|
||||
err = OnBoardUser(user)
|
||||
if err != nil {
|
||||
|
@ -194,7 +194,7 @@ func (l *Auth) SearchUser(username string) (*models.User, error) {
|
||||
|
||||
log.Debugf("Found ldap user %v", user)
|
||||
} else {
|
||||
return nil, fmt.Errorf("no user found, %v", username)
|
||||
return nil, errors.NotFoundError(nil).WithMessage("no user found: %v", username)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
@ -224,7 +224,7 @@ func (l *Auth) SearchGroup(groupKey string) (*ugModel.UserGroup, error) {
|
||||
}
|
||||
|
||||
if len(userGroupList) == 0 {
|
||||
return nil, fmt.Errorf("failed to searh ldap group with groupDN:%v", groupKey)
|
||||
return nil, errors.NotFoundError(nil).WithMessage("failed to searh ldap group with groupDN:%v", groupKey)
|
||||
}
|
||||
userGroup := ugModel.UserGroup{
|
||||
GroupName: userGroupList[0].Name,
|
||||
@ -244,7 +244,7 @@ func (l *Auth) OnBoardGroup(u *ugModel.UserGroup, altGroupName string) error {
|
||||
}
|
||||
u.GroupType = common.LDAPGroupType
|
||||
// Check duplicate LDAP DN in usergroup, if usergroup exist, return error
|
||||
userGroupList, err := ugCtl.Ctl.List(ctx, ugModel.UserGroup{LdapGroupDN: u.LdapGroupDN})
|
||||
userGroupList, err := ugCtl.Ctl.List(ctx, q.New(q.KeyWords{"LdapGroupDN": u.LdapGroupDN}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup/model"
|
||||
"time"
|
||||
)
|
||||
@ -35,8 +36,10 @@ func init() {
|
||||
type DAO interface {
|
||||
// Add add user group
|
||||
Add(ctx context.Context, userGroup model.UserGroup) (int, error)
|
||||
// Count query user group count
|
||||
Count(ctx context.Context, query *q.Query) (int64, error)
|
||||
// Query query user group
|
||||
Query(ctx context.Context, query model.UserGroup) ([]*model.UserGroup, error)
|
||||
Query(ctx context.Context, query *q.Query) ([]*model.UserGroup, error)
|
||||
// Get get user group by id
|
||||
Get(ctx context.Context, id int) (*model.UserGroup, error)
|
||||
// Delete delete user group by id
|
||||
@ -60,7 +63,8 @@ var ErrGroupNameDup = errors.ConflictError(nil).WithMessage("duplicated user gro
|
||||
|
||||
// Add - Add User Group
|
||||
func (d *dao) Add(ctx context.Context, userGroup model.UserGroup) (int, error) {
|
||||
userGroupList, err := d.Query(ctx, model.UserGroup{GroupName: userGroup.GroupName, GroupType: common.HTTPGroupType})
|
||||
query := q.New(q.KeyWords{"GroupName": userGroup.GroupName, "GroupType": common.HTTPGroupType})
|
||||
userGroupList, err := d.Query(ctx, query)
|
||||
if err != nil {
|
||||
return 0, ErrGroupNameDup
|
||||
}
|
||||
@ -84,43 +88,22 @@ func (d *dao) Add(ctx context.Context, userGroup model.UserGroup) (int, error) {
|
||||
}
|
||||
|
||||
// Query - Query User Group
|
||||
func (d *dao) Query(ctx context.Context, query model.UserGroup) ([]*model.UserGroup, error) {
|
||||
o, err := orm.FromContext(ctx)
|
||||
func (d *dao) Query(ctx context.Context, query *q.Query) ([]*model.UserGroup, error) {
|
||||
query = q.MustClone(query)
|
||||
qs, err := orm.QuerySetter(ctx, &model.UserGroup{}, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sql := `select id, group_name, group_type, ldap_group_dn from user_group where 1=1 `
|
||||
sqlParam := make([]interface{}, 1)
|
||||
var groups []*model.UserGroup
|
||||
if len(query.GroupName) != 0 {
|
||||
sql += ` and group_name = ? `
|
||||
sqlParam = append(sqlParam, query.GroupName)
|
||||
}
|
||||
|
||||
if query.GroupType != 0 {
|
||||
sql += ` and group_type = ? `
|
||||
sqlParam = append(sqlParam, query.GroupType)
|
||||
}
|
||||
|
||||
if len(query.LdapGroupDN) != 0 {
|
||||
sql += ` and ldap_group_dn = ? `
|
||||
sqlParam = append(sqlParam, utils.TrimLower(query.LdapGroupDN))
|
||||
}
|
||||
if query.ID != 0 {
|
||||
sql += ` and id = ? `
|
||||
sqlParam = append(sqlParam, query.ID)
|
||||
}
|
||||
_, err = o.Raw(sql, sqlParam).QueryRows(&groups)
|
||||
if err != nil {
|
||||
var usergroups []*model.UserGroup
|
||||
if _, err := qs.All(&usergroups); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groups, nil
|
||||
return usergroups, nil
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (d *dao) Get(ctx context.Context, id int) (*model.UserGroup, error) {
|
||||
userGroup := model.UserGroup{ID: id}
|
||||
userGroupList, err := d.Query(ctx, userGroup)
|
||||
userGroupList, err := d.Query(ctx, q.New(q.KeyWords{"ID": id}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -192,3 +175,12 @@ func (d *dao) onBoardCommonUserGroup(ctx context.Context, g *model.UserGroup, ke
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
query = q.MustClone(query)
|
||||
qs, err := orm.QuerySetterForCount(ctx, &model.UserGroup{}, query)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return qs.Count()
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"errors"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup/model"
|
||||
)
|
||||
@ -35,7 +36,9 @@ type Manager interface {
|
||||
// Create create user group
|
||||
Create(ctx context.Context, userGroup model.UserGroup) (int, error)
|
||||
// List list user group
|
||||
List(ctx context.Context, query model.UserGroup) ([]*model.UserGroup, error)
|
||||
List(ctx context.Context, query *q.Query) ([]*model.UserGroup, error)
|
||||
// Count get user group count
|
||||
Count(ctx context.Context, query *q.Query) (int64, error)
|
||||
// Get get user group by id
|
||||
Get(ctx context.Context, id int) (*model.UserGroup, error)
|
||||
// Populate populate user group from external auth server to Harbor and return the group id
|
||||
@ -57,11 +60,7 @@ func newManager() Manager {
|
||||
}
|
||||
|
||||
func (m *manager) Create(ctx context.Context, userGroup model.UserGroup) (int, error) {
|
||||
query := model.UserGroup{
|
||||
GroupName: userGroup.GroupName,
|
||||
GroupType: userGroup.GroupType,
|
||||
}
|
||||
ug, err := m.dao.Query(ctx, query)
|
||||
ug, err := m.dao.Query(ctx, q.New(q.KeyWords{"GroupName": userGroup.GroupName, "GroupType": userGroup.GroupType}))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -71,7 +70,7 @@ func (m *manager) Create(ctx context.Context, userGroup model.UserGroup) (int, e
|
||||
return m.dao.Add(ctx, userGroup)
|
||||
}
|
||||
|
||||
func (m *manager) List(ctx context.Context, query model.UserGroup) ([]*model.UserGroup, error) {
|
||||
func (m *manager) List(ctx context.Context, query *q.Query) ([]*model.UserGroup, error) {
|
||||
return m.dao.Query(ctx, query)
|
||||
}
|
||||
|
||||
@ -127,3 +126,7 @@ func (m *manager) onBoardCommonUserGroup(ctx context.Context, g *model.UserGroup
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
return m.dao.Count(ctx, query)
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
package usergroup
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup/model"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -41,8 +42,7 @@ func (s *ManagerTestSuite) TestOnboardGroup() {
|
||||
}
|
||||
err := s.mgr.Onboard(ctx, ug)
|
||||
s.Nil(err)
|
||||
qm := model.UserGroup{GroupType: 1, LdapGroupDN: "cn=harbor_dev,ou=groups,dc=example,dc=com"}
|
||||
ugs, err := s.mgr.List(ctx, qm)
|
||||
ugs, err := s.mgr.List(ctx, q.New(q.KeyWords{"GroupType": 1, "LdapGroupDN": "cn=harbor_dev,ou=groups,dc=example,dc=com"}))
|
||||
s.Nil(err)
|
||||
s.True(len(ugs) > 0)
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
ugCtl "github.com/goharbor/harbor/src/controller/usergroup"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/usergroup/model"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/usergroup"
|
||||
@ -105,22 +106,35 @@ func (u *userGroupAPI) ListUserGroups(ctx context.Context, params operation.List
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
query := model.UserGroup{}
|
||||
query, err := u.BuildQuery(ctx, nil, nil, params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
switch authMode {
|
||||
case common.LDAPAuth:
|
||||
query.GroupType = common.LDAPGroupType
|
||||
query.Keywords["GroupType"] = common.LDAPGroupType
|
||||
if params.LdapGroupDn != nil && len(*params.LdapGroupDn) > 0 {
|
||||
query.LdapGroupDN = *params.LdapGroupDn
|
||||
query.Keywords["LdapGroupDN"] = *params.LdapGroupDn
|
||||
}
|
||||
case common.HTTPAuth:
|
||||
query.GroupType = common.HTTPGroupType
|
||||
query.Keywords["GroupType"] = common.HTTPGroupType
|
||||
}
|
||||
|
||||
total, err := u.ctl.Count(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if total == 0 {
|
||||
return operation.NewListUserGroupsOK().WithXTotalCount(0).WithPayload([]*models.UserGroup{})
|
||||
}
|
||||
ug, err := u.ctl.List(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewListUserGroupsOK().WithPayload(getUserGroupResp(ug))
|
||||
return operation.NewListUserGroupsOK().
|
||||
WithXTotalCount(total).
|
||||
WithPayload(getUserGroupResp(ug)).
|
||||
WithLink(u.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String())
|
||||
}
|
||||
func getUserGroupResp(ug []*model.UserGroup) []*models.UserGroup {
|
||||
result := make([]*models.UserGroup, 0)
|
||||
@ -135,6 +149,18 @@ func getUserGroupResp(ug []*model.UserGroup) []*models.UserGroup {
|
||||
}
|
||||
return result
|
||||
}
|
||||
func getUserGroupSearchItem(ug []*model.UserGroup) []*models.UserGroupSearchItem {
|
||||
result := make([]*models.UserGroupSearchItem, 0)
|
||||
for _, u := range ug {
|
||||
ug := &models.UserGroupSearchItem{
|
||||
GroupName: u.GroupName,
|
||||
GroupType: int64(u.GroupType),
|
||||
ID: int64(u.ID),
|
||||
}
|
||||
result = append(result, ug)
|
||||
}
|
||||
return result
|
||||
}
|
||||
func (u *userGroupAPI) UpdateUserGroup(ctx context.Context, params operation.UpdateUserGroupParams) middleware.Responder {
|
||||
if err := u.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceUserGroup); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
@ -151,3 +177,31 @@ func (u *userGroupAPI) UpdateUserGroup(ctx context.Context, params operation.Upd
|
||||
}
|
||||
return operation.NewUpdateUserGroupOK()
|
||||
}
|
||||
|
||||
func (u *userGroupAPI) SearchUserGroups(ctx context.Context, params operation.SearchUserGroupsParams) middleware.Responder {
|
||||
if err := u.RequireAuthenticated(ctx); err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
query, err := u.BuildQuery(ctx, nil, nil, params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if len(params.Groupname) == 0 {
|
||||
return u.SendError(ctx, errors.BadRequestError(nil).WithMessage("need to provide groupname to search user group"))
|
||||
}
|
||||
query.Keywords["GroupName"] = &q.FuzzyMatchValue{Value: params.Groupname}
|
||||
total, err := u.ctl.Count(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
if total == 0 {
|
||||
return operation.NewSearchUserGroupsOK().WithXTotalCount(0).WithPayload([]*models.UserGroupSearchItem{})
|
||||
}
|
||||
ug, err := u.ctl.List(ctx, query)
|
||||
if err != nil {
|
||||
return u.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewSearchUserGroupsOK().WithXTotalCount(total).
|
||||
WithPayload(getUserGroupSearchItem(ug)).
|
||||
WithLink(u.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user