From 44d0275864b10ce7b8f87c1be1f02017ddf10804 Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 10 Dec 2025 15:32:23 +0200 Subject: [PATCH 1/8] Implement getGroups v3 api --- core/interfaces.go | 8 +-- core/interfaces_admin.go | 2 +- core/interfaces_client.go | 4 +- core/services.go | 19 ++----- docs/docs.go | 93 +++++++------------------------- docs/swagger.json | 93 +++++++------------------------- docs/swagger.yaml | 72 ++++++------------------- driven/storage/adapter.go | 79 ++++++++++++++++----------- driver/web/adapter.go | 4 ++ driver/web/rest/admin_apis.go | 4 +- driver/web/rest/admin_apis_v2.go | 67 ++++++++++++++++++++++- driver/web/rest/apis.go | 2 +- driver/web/rest/apis_v2.go | 62 ++++++++++++++++++++- driver/web/rest/internal.go | 2 +- 14 files changed, 248 insertions(+), 263 deletions(-) diff --git a/core/interfaces.go b/core/interfaces.go index aaa0b622..cd22c2ec 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -39,8 +39,8 @@ type Services interface { UpdateGroupDateUpdated(clientID string, groupID string) error DeleteGroup(clientID string, current *model.User, id string) error GetAllGroupsUnsecured() ([]model.Group, error) - GetAllGroups(clientID string) ([]model.Group, error) - GetGroups(clientID string, current *model.User, filter model.GroupsFilter) ([]model.Group, error) + GetAllGroups(clientID string) (int64, []model.Group, error) + GetGroups(clientID string, current *model.User, filter model.GroupsFilter) (int64, []model.Group, error) GetGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter) (*model.StatsResult, error) GetUserGroups(clientID string, current *model.User, filter model.GroupsFilter) ([]model.Group, error) DeleteUser(clientID string, current *model.User) error @@ -122,7 +122,7 @@ type Services interface { // Administration exposes administration APIs for the driver adapters type Administration interface { - GetGroups(clientID string, current *model.User, filter model.GroupsFilter) ([]model.Group, error) + GetGroups(clientID string, current *model.User, filter model.GroupsFilter) (int64, []model.Group, error) DeleteGroup(clientID string, current *model.User, id string, inactive bool) error AdminDeleteMembershipsByID(clientID string, current *model.User, groupID string, accountIDs []string) error } @@ -159,7 +159,7 @@ type Storage interface { DeleteGroup(ctx storage.TransactionContext, clientID string, id string) error FindGroup(context storage.TransactionContext, clientID string, groupID string, userID *string) (*model.Group, error) FindGroupByTitle(clientID string, title string) (*model.Group, error) - FindGroups(clientID string, userID *string, filter model.GroupsFilter, skipMembershipCheck bool) ([]model.Group, error) + FindGroups(clientID string, userID *string, filter model.GroupsFilter, skipMembershipCheck bool) (int64, []model.Group, error) FindAllGroupsUnsecured() ([]model.Group, error) FindGroupsByGroupIDs(groupIDs []string) ([]model.Group, error) FindUserGroups(clientID string, userID string, filter model.GroupsFilter) ([]model.Group, error) diff --git a/core/interfaces_admin.go b/core/interfaces_admin.go index 645f93ab..0956828e 100644 --- a/core/interfaces_admin.go +++ b/core/interfaces_admin.go @@ -22,7 +22,7 @@ type administrationImpl struct { app *Application } -func (s *administrationImpl) GetGroups(clientID string, current *model.User, filter model.GroupsFilter) ([]model.Group, error) { +func (s *administrationImpl) GetGroups(clientID string, current *model.User, filter model.GroupsFilter) (int64, []model.Group, error) { skipMembershipCheck := false if current != nil { skipMembershipCheck = current.IsGroupsBBAdministrator() diff --git a/core/interfaces_client.go b/core/interfaces_client.go index 4889b8af..81947920 100644 --- a/core/interfaces_client.go +++ b/core/interfaces_client.go @@ -61,7 +61,7 @@ func (s *servicesImpl) DeleteGroup(clientID string, current *model.User, id stri return s.app.deleteGroup(clientID, current, id, false) } -func (s *servicesImpl) GetGroups(clientID string, current *model.User, filter model.GroupsFilter) ([]model.Group, error) { +func (s *servicesImpl) GetGroups(clientID string, current *model.User, filter model.GroupsFilter) (int64, []model.Group, error) { return s.app.getGroups(clientID, current, filter, false) } @@ -69,7 +69,7 @@ func (s *servicesImpl) GetAllGroupsUnsecured() ([]model.Group, error) { return s.app.getAllGroupsUnsecured() } -func (s *servicesImpl) GetAllGroups(clientID string) ([]model.Group, error) { +func (s *servicesImpl) GetAllGroups(clientID string) (int64, []model.Group, error) { return s.app.getAllGroups(clientID) } diff --git a/core/services.go b/core/services.go index 7d08ee91..07a93305 100644 --- a/core/services.go +++ b/core/services.go @@ -418,28 +418,17 @@ func (app *Application) getAllGroupsUnsecured() ([]model.Group, error) { return app.storage.FindAllGroupsUnsecured() } -func (app *Application) getGroups(clientID string, current *model.User, filter model.GroupsFilter, skipMembershipCheck bool) ([]model.Group, error) { +func (app *Application) getGroups(clientID string, current *model.User, filter model.GroupsFilter, skipMembershipCheck bool) (int64, []model.Group, error) { var userID *string if current != nil { userID = ¤t.ID } // find the groups objects - groups, err := app.storage.FindGroups(clientID, userID, filter, skipMembershipCheck) - if err != nil { - return nil, err - } - - return groups, nil + return app.storage.FindGroups(clientID, userID, filter, skipMembershipCheck) } -func (app *Application) getAllGroups(clientID string) ([]model.Group, error) { - // find the groups objects - groups, err := app.storage.FindGroups(clientID, nil, model.GroupsFilter{}, false) - if err != nil { - return nil, err - } - - return groups, nil +func (app *Application) getAllGroups(clientID string) (int64, []model.Group, error) { + return app.storage.FindGroups(clientID, nil, model.GroupsFilter{}, false) } func (app *Application) getUserGroups(clientID string, current *model.User, filter model.GroupsFilter) ([]model.Group, error) { diff --git a/docs/docs.go b/docs/docs.go index 246b9e1d..1ed7b838 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1516,64 +1516,15 @@ const docTemplate = `{ "AppUserAuth": [] } ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", + "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", "consumes": [ "application/json" ], "tags": [ "Admin" ], - "operationId": "AdminGetGroupsV2", + "operationId": "AdminGetGroupsV3", "parameters": [ - { - "type": "string", - "description": "APP", - "name": "APP", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)", - "name": "title", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! category - filter by category", - "name": "category", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! privacy - filter by privacy", - "name": "privacy", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! offset - skip number of records", - "name": "offset", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! limit - limit the result", - "name": "limit", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - Filter by number of days inactive", - "name": "days_inactive", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false.", - "name": "include_hidden", - "in": "query" - }, { "description": "body data", "name": "data", @@ -1588,10 +1539,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Group" - } + "$ref": "#/definitions/getGroupsResponseV3" } } } @@ -5239,12 +5187,7 @@ const docTemplate = `{ "type": "object", "additionalProperties": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } + "additionalProperties": {} } }, "settings": { @@ -5965,12 +5908,7 @@ const docTemplate = `{ "type": "object", "additionalProperties": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } + "additionalProperties": {} } }, "settings": { @@ -6165,6 +6103,20 @@ const docTemplate = `{ } } }, + "getGroupsResponseV3": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + }, + "total_count": { + "type": "integer" + } + } + }, "getPutAdminGroupIDsForEventIDRequestAndResponse": { "type": "object", "properties": { @@ -6807,12 +6759,7 @@ const docTemplate = `{ "type": "object", "additionalProperties": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } + "additionalProperties": {} } }, "settings": { diff --git a/docs/swagger.json b/docs/swagger.json index 5a4f8496..eb2fff96 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1513,64 +1513,15 @@ "AppUserAuth": [] } ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", + "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", "consumes": [ "application/json" ], "tags": [ "Admin" ], - "operationId": "AdminGetGroupsV2", + "operationId": "AdminGetGroupsV3", "parameters": [ - { - "type": "string", - "description": "APP", - "name": "APP", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)", - "name": "title", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! category - filter by category", - "name": "category", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! privacy - filter by privacy", - "name": "privacy", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! offset - skip number of records", - "name": "offset", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! limit - limit the result", - "name": "limit", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - Filter by number of days inactive", - "name": "days_inactive", - "in": "query" - }, - { - "type": "string", - "description": "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false.", - "name": "include_hidden", - "in": "query" - }, { "description": "body data", "name": "data", @@ -1585,10 +1536,7 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Group" - } + "$ref": "#/definitions/getGroupsResponseV3" } } } @@ -5236,12 +5184,7 @@ "type": "object", "additionalProperties": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } + "additionalProperties": {} } }, "settings": { @@ -5962,12 +5905,7 @@ "type": "object", "additionalProperties": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } + "additionalProperties": {} } }, "settings": { @@ -6162,6 +6100,20 @@ } } }, + "getGroupsResponseV3": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + }, + "total_count": { + "type": "integer" + } + } + }, "getPutAdminGroupIDsForEventIDRequestAndResponse": { "type": "object", "properties": { @@ -6804,12 +6756,7 @@ "type": "object", "additionalProperties": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } + "additionalProperties": {} } }, "settings": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 089cfe0c..08db382f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -130,10 +130,7 @@ definitions: type: boolean research_profile: additionalProperties: - additionalProperties: - items: - type: string - type: array + additionalProperties: {} type: object type: object settings: @@ -618,10 +615,7 @@ definitions: type: boolean research_profile: additionalProperties: - additionalProperties: - items: - type: string - type: array + additionalProperties: {} type: object type: object settings: @@ -752,6 +746,15 @@ definitions: web_url: type: string type: object + getGroupsResponseV3: + properties: + results: + items: + $ref: '#/definitions/Group' + type: array + total_count: + type: integer + type: object getPutAdminGroupIDsForEventIDRequestAndResponse: properties: group_ids: @@ -1179,10 +1182,7 @@ definitions: type: boolean research_profile: additionalProperties: - additionalProperties: - items: - type: string - type: array + additionalProperties: {} type: object type: object settings: @@ -2190,49 +2190,9 @@ paths: consumes: - application/json description: Gives the groups list. It can be filtered by category, title and - privacy. V2 - operationId: AdminGetGroupsV2 + privacy. V3 + operationId: AdminGetGroupsV3 parameters: - - description: APP - in: header - name: APP - required: true - type: string - - description: Deprecated - instead use request body filter! Filtering by group's - title (case-insensitive) - in: query - name: title - type: string - - description: Deprecated - instead use request body filter! category - filter - by category - in: query - name: category - type: string - - description: Deprecated - instead use request body filter! privacy - filter - by privacy - in: query - name: privacy - type: string - - description: Deprecated - instead use request body filter! offset - skip number - of records - in: query - name: offset - type: string - - description: Deprecated - instead use request body filter! limit - limit the - result - in: query - name: limit - type: string - - description: Deprecated - Filter by number of days inactive - in: query - name: days_inactive - type: string - - description: Deprecated - instead use request body filter! include_hidden - - Includes hidden groups if a search by title is performed. Possible value - is true. Default false. - in: query - name: include_hidden - type: string - description: body data in: body name: data @@ -2243,9 +2203,7 @@ paths: "200": description: OK schema: - items: - $ref: '#/definitions/Group' - type: array + $ref: '#/definitions/getGroupsResponseV3' security: - AppUserAuth: [] tags: diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 5a779926..2532af4c 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -647,9 +647,54 @@ func (sa *Adapter) FindGroupByTitle(clientID string, title string) (*model.Group } // FindGroups finds groups -func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) ([]model.Group, error) { +func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) (int64, []model.Group, error) { // TODO: Merge the filter logic in a common method (FindGroups, FindGroupsV3, FindUserGroups) + filter, err := sa.buildMainQuery(userID, clientID, groupsFilter, skipMembershipCheck) + if err != nil { + return 0, nil, err + } + + var findOptions options.FindOptions + if groupsFilter.Limit != nil { + findOptions.SetLimit(*groupsFilter.Limit) + } + if groupsFilter.Offset != nil { + findOptions.SetSkip(*groupsFilter.Offset) + } + + findOptions.SetCollation(&options.Collation{ + Locale: "en", + Strength: 1, // Case and diacritic insensitive§ + }) + + count, err := sa.db.groups.CountDocuments(filter) + if err != nil { + return 0, nil, err + } + + var list []model.Group + var memberships model.MembershipCollection + err = sa.db.groups.Find(filter, &list, &findOptions) + if err != nil { + return 0, nil, err + } + + if userID != nil { + for index, group := range list { + group.CurrentMember = memberships.GetMembershipBy(func(membership model.GroupMembership) bool { + return membership.GroupID == group.ID + }) + if group.CurrentMember != nil { + list[index] = group + } + } + } + + return count, list, nil +} + +func (sa *Adapter) buildMainQuery(userID *string, clientID string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) (bson.D, error) { var err error groupIDs := []string{} var memberships model.MembershipCollection @@ -795,37 +840,7 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode pastTime := time.Now().Add(time.Duration(*groupsFilter.DaysInactive) * -24 * time.Hour) filter = append(filter, primitive.E{Key: "date_updated", Value: bson.M{"$lt": pastTime}}) } - - if groupsFilter.Limit != nil { - findOptions.SetLimit(*groupsFilter.Limit) - } - if groupsFilter.Offset != nil { - findOptions.SetSkip(*groupsFilter.Offset) - } - - findOptions.SetCollation(&options.Collation{ - Locale: "en", - Strength: 1, // Case and diacritic insensitive - }) - - var list []model.Group - err = sa.db.groups.Find(filter, &list, findOptions) - if err != nil { - return nil, err - } - - if userID != nil { - for index, group := range list { - group.CurrentMember = memberships.GetMembershipBy(func(membership model.GroupMembership) bool { - return membership.GroupID == group.ID - }) - if group.CurrentMember != nil { - list[index] = group - } - } - } - - return list, nil + return filter, nil } // FindGroupByID finds one groups by ID and clientID diff --git a/driver/web/adapter.go b/driver/web/adapter.go index 26a2b9fb..0449e042 100644 --- a/driver/web/adapter.go +++ b/driver/web/adapter.go @@ -89,6 +89,9 @@ func (we *Adapter) Start() { adminSubrouter.HandleFunc("/v2/user/groups", we.adminIDTokenAuthWrapFunc(we.adminApisHandler.GetUserGroupsV2)).Methods("GET", "POST") adminSubrouter.HandleFunc("/v2/group/{id}", we.adminIDTokenAuthWrapFunc(we.adminApisHandler.GetGroupV2)).Methods("GET") + // Admin V3 APIs + adminSubrouter.HandleFunc("/v3/groups/load", we.adminIDTokenAuthWrapFunc(we.adminApisHandler.GetGroupsV3)).Methods("POST") + // Admin V1 APIs adminSubrouter.HandleFunc("/authman/synchronize", we.adminIDTokenAuthWrapFunc(we.adminApisHandler.SynchronizeAuthman)).Methods("POST") adminSubrouter.HandleFunc("/user/groups", we.adminIDTokenAuthWrapFunc(we.adminApisHandler.GetUserGroups)).Methods("GET") @@ -140,6 +143,7 @@ func (we *Adapter) Start() { restSubrouter.HandleFunc("/v2/user/groups", we.idTokenAuthWrapFunc(we.apisHandler.GetUserGroupsV2)).Methods("GET", "POST") restSubrouter.HandleFunc("/v3/group", we.idTokenAuthWrapFunc(we.apisHandler.CreateGroupV3)).Methods("POST") + restSubrouter.HandleFunc("/v3/groups/load", we.anonymousAuthWrapFunc(we.apisHandler.GetGroupsV3)).Methods("POST") //V1 Client APIs restSubrouter.HandleFunc("/authman/synchronize", we.idTokenAuthWrapFunc(we.apisHandler.SynchronizeAuthman)).Methods("POST") diff --git a/driver/web/rest/admin_apis.go b/driver/web/rest/admin_apis.go index 3d54cfee..34b4b264 100644 --- a/driver/web/rest/admin_apis.go +++ b/driver/web/rest/admin_apis.go @@ -102,7 +102,7 @@ func (h *AdminApisHandler) GetUserGroups(clientID string, current *model.User, w groupsFilter.ResearchGroup = &b } - groups, err := h.app.Services.GetGroups(clientID, current, groupsFilter) + _, groups, err := h.app.Services.GetGroups(clientID, current, groupsFilter) if err != nil { log.Printf("error getting groups - %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -224,7 +224,7 @@ func (h *AdminApisHandler) GetAllGroups(clientID string, current *model.User, w groupsFilter.ResearchGroup = &b } - groups, err := h.app.Services.GetGroups(clientID, nil, groupsFilter) + _, groups, err := h.app.Services.GetGroups(clientID, nil, groupsFilter) if err != nil { log.Printf("adminapis.GetAllGroups() error getting groups - %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/driver/web/rest/admin_apis_v2.go b/driver/web/rest/admin_apis_v2.go index d4c56dac..a1b0b906 100644 --- a/driver/web/rest/admin_apis_v2.go +++ b/driver/web/rest/admin_apis_v2.go @@ -119,7 +119,7 @@ func (h *AdminApisHandler) GetGroupsV2(clientID string, current *model.User, w h groupsFilter.ResearchGroup = &b } - groups, err := h.app.Admin.GetGroups(clientID, current, groupsFilter) + _, groups, err := h.app.Admin.GetGroups(clientID, current, groupsFilter) if err != nil { log.Printf("adminapis.GetGroupsV2() error getting groups - %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -142,6 +142,71 @@ func (h *AdminApisHandler) GetGroupsV2(clientID string, current *model.User, w h w.Write(data) } +type getGroupsResponseV3 struct { + Groups []model.Group `json:"results"` + Total int64 `json:"total_count"` +} // @name getGroupsResponseV3 + +// GetGroupsV3 gets groups. It can be filtered by category, title and privacy. V3 +// @Description Gives the groups list. It can be filtered by category, title and privacy. V3 +// @ID AdminGetGroupsV3 +// @Tags Admin +// @Accept json +// @Param data body model.GroupsFilter true "body data" +// @Success 200 {object} getGroupsResponseV3 +// @Security AppUserAuth +// @Router /api/admin/v2/groups [post] +func (h *AdminApisHandler) GetGroupsV3(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { + var groupsFilter model.GroupsFilter + + requestData, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("adminapis.GetGroupsV3() error on marshal model.GroupsFilter request body - %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if len(requestData) > 0 { + err = json.Unmarshal(requestData, &groupsFilter) + if err != nil { + // just log an error and proceed and assume an empty filter + log.Printf("adminapis.GetGroupsV3() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) + } + } + + if groupsFilter.ResearchGroup == nil { + b := false + groupsFilter.ResearchGroup = &b + } + + _, groups, err := h.app.Admin.GetGroups(clientID, current, groupsFilter) + if err != nil { + log.Printf("adminapis.GetGroupsV3() error getting groups - %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if groups == nil { + groups = []model.Group{} + } + + result := getGroupsResponseV3{ + Groups: groups, + Total: int64(len(groups)), + } + + data, err := json.Marshal(result) + if err != nil { + log.Println("adminapis.GetGroupsV3() error on marshal the groups items") + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(data) +} + // GetUserGroupsV2 gets the user groups. It can be filtered by category, title and privacy. V2. // @Description Gives the user groups. It can be filtered by category, title and privacy. V2. // @ID AdminGetUserGroupsV2 diff --git a/driver/web/rest/apis.go b/driver/web/rest/apis.go index 7bf7001b..d8860a14 100644 --- a/driver/web/rest/apis.go +++ b/driver/web/rest/apis.go @@ -603,7 +603,7 @@ func (h *ApisHandler) GetGroups(orgID string, current *model.User, w http.Respon groupsFilter.ResearchGroup = &b } - groups, err := h.app.Services.GetGroups(orgID, current, groupsFilter) + _, groups, err := h.app.Services.GetGroups(orgID, current, groupsFilter) if err != nil { log.Printf("apis.GetGroups() error getting groups - %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/driver/web/rest/apis_v2.go b/driver/web/rest/apis_v2.go index c92a2a5d..66186b6c 100644 --- a/driver/web/rest/apis_v2.go +++ b/driver/web/rest/apis_v2.go @@ -111,7 +111,7 @@ func (h *ApisHandler) GetGroupsV2(clientID string, current *model.User, w http.R groupsFilter.ResearchGroup = &b } - groups, err := h.app.Services.GetGroups(clientID, current, groupsFilter) + _, groups, err := h.app.Services.GetGroups(clientID, current, groupsFilter) if err != nil { log.Printf("apis.GetGroupsV2() error getting groups - %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -134,6 +134,66 @@ func (h *ApisHandler) GetGroupsV2(clientID string, current *model.User, w http.R w.Write(data) } +// GetGroupsV3 gets groups. It can be filtered by category, title and privacy. V3 +// @Description Gives the groups list. It can be filtered by category, title and privacy. V3 +// @ID GetGroupsV3 +// @Tags Admin +// @Accept json +// @Param data body model.GroupsFilter true "body data" +// @Success 200 {object} getGroupsResponseV3 +// @Security AppUserAuth +// @Router /api/admin/v2/groups [post] +func (h *ApisHandler) GetGroupsV3(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { + var groupsFilter model.GroupsFilter + + requestData, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("adminapis.GetGroupsV3() error on marshal model.GroupsFilter request body - %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if len(requestData) > 0 { + err = json.Unmarshal(requestData, &groupsFilter) + if err != nil { + // just log an error and proceed and assume an empty filter + log.Printf("adminapis.GetGroupsV3() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) + } + } + + if groupsFilter.ResearchGroup == nil { + b := false + groupsFilter.ResearchGroup = &b + } + + count, groups, err := h.app.Admin.GetGroups(clientID, current, groupsFilter) + if err != nil { + log.Printf("adminapis.GetGroupsV3() error getting groups - %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if groups == nil { + groups = []model.Group{} + } + + result := getGroupsResponseV3{ + Groups: groups, + Total: count, + } + + data, err := json.Marshal(result) + if err != nil { + log.Println("adminapis.GetGroupsV3() error on marshal the groups items") + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(data) +} + // GetUserGroupsV2 gets the user groups. It can be filtered by category, title and privacy. V2. // @Description Gives the user groups. It can be filtered by category, title and privacy. V2. // @ID GetUserGroupsV2 diff --git a/driver/web/rest/internal.go b/driver/web/rest/internal.go index cac102ab..d20768eb 100644 --- a/driver/web/rest/internal.go +++ b/driver/web/rest/internal.go @@ -257,7 +257,7 @@ type GroupStat struct { // @Router /int/stats [get] func (h *InternalApisHandler) GroupStats(clientID string, w http.ResponseWriter, r *http.Request) { - groups, err := h.app.Services.GetAllGroups(clientID) + _, groups, err := h.app.Services.GetAllGroups(clientID) if err != nil { log.Printf("Error GroupStats(%s): %s", clientID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) From a3caa1bfe368175edb97ed43d3518607939e5734 Mon Sep 17 00:00:00 2001 From: Mladen Date: Thu, 11 Dec 2025 18:34:33 +0200 Subject: [PATCH 2/8] Finish the feature and resolve vulnerabilities --- core/model/filters.go | 8 +- driven/storage/adapter.go | 161 ++++++++++++++++++++++++----------- driven/storage/adapter_v3.go | 8 +- go.mod | 41 +++++---- go.sum | 79 ++++++++--------- 5 files changed, 182 insertions(+), 115 deletions(-) diff --git a/core/model/filters.go b/core/model/filters.go index 191497ec..bf2a97ba 100644 --- a/core/model/filters.go +++ b/core/model/filters.go @@ -21,6 +21,7 @@ type GroupsFilter struct { MemberID *string `json:"member_id"` // member id MemberUserID *string `json:"member_user_id"` // member user id MemberExternalID *string `json:"member_external_id"` // member user external id + MemberStatuses []string `json:"member_statuses"` // member user status Title *string `json:"title"` // group title Category *string `json:"category"` // group category Privacy *string `json:"privacy"` // group privacy @@ -33,9 +34,10 @@ type GroupsFilter struct { ResearchGroup *bool `json:"research_group"` ResearchAnswers map[string]map[string][]string `json:"research_answers"` Attributes map[string]interface{} `json:"attributes"` - Order *string `json:"order"` // order by category & name (asc desc) - Offset *int64 `json:"offset"` // result offset - Limit *int64 `json:"limit"` // result limit + Order *string `json:"order"` // order by category & name (asc desc) + Offset *int64 `json:"offset"` // result offset + Limit *int64 `json:"limit"` // result limit + LimitID *string `json:"limit_id"` // limit id DaysInactive *int64 `json:"days_inactive"` Administrative *bool `json:"administrative"` } // @name GroupsFilter diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 2532af4c..9e634aab 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -649,64 +649,139 @@ func (sa *Adapter) FindGroupByTitle(clientID string, title string) (*model.Group // FindGroups finds groups func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) (int64, []model.Group, error) { // TODO: Merge the filter logic in a common method (FindGroups, FindGroupsV3, FindUserGroups) + var count int64 + var list []model.Group + err := sa.PerformTransaction(func(ctx TransactionContext) error { + filter, err := sa.buildMainQuery(ctx, userID, clientID, groupsFilter, skipMembershipCheck) + if err != nil { + return err + } - filter, err := sa.buildMainQuery(userID, clientID, groupsFilter, skipMembershipCheck) - if err != nil { - return 0, nil, err - } + type rowNumber struct { + RowNumber int `json:"_row_number" bson:"_row_number"` + } - var findOptions options.FindOptions - if groupsFilter.Limit != nil { - findOptions.SetLimit(*groupsFilter.Limit) - } - if groupsFilter.Offset != nil { - findOptions.SetSkip(*groupsFilter.Offset) - } + var aggrSort bson.D + if groupsFilter.Order != nil && "desc" == *groupsFilter.Order { + aggrSort = bson.D{ + {Key: "_c_title", Value: -1}, + } + } else { + aggrSort = bson.D{ + {Key: "_c_title", Value: 1}, + } - findOptions.SetCollation(&options.Collation{ - Locale: "en", - Strength: 1, // Case and diacritic insensitive§ - }) + } - count, err := sa.db.groups.CountDocuments(filter) - if err != nil { - return 0, nil, err - } + var limitIDRowNumber int + if groupsFilter.LimitID != nil { + var rowNumbers []rowNumber + err := sa.db.groups.AggregateWithContext(ctx, bson.A{ + bson.D{{Key: "$match", Value: filter}}, + bson.D{{Key: "$addFields", Value: bson.M{"_c_title": bson.M{"$toUpper": "$title"}}}}, + bson.D{{Key: "$sort", Value: aggrSort}}, + //bson.D{{Key: "$skip", Value: skip}}, + bson.D{ + {Key: "$setWindowFields", + Value: bson.D{ + {Key: "sortBy", Value: aggrSort}, + {Key: "output", Value: bson.D{{Key: "_row_number", Value: bson.D{{Key: "$documentNumber", Value: bson.D{}}}}}}, + }, + }, + }, + bson.D{{Key: "$match", Value: bson.D{{Key: "_id", Value: *groupsFilter.LimitID}}}}, + }, &rowNumbers, nil) + if err != nil { + return err + } - var list []model.Group - var memberships model.MembershipCollection - err = sa.db.groups.Find(filter, &list, &findOptions) - if err != nil { - return 0, nil, err - } + if rowNumbers[0].RowNumber != 0 { + limitIDRowNumber = rowNumbers[0].RowNumber + } + } - if userID != nil { - for index, group := range list { - group.CurrentMember = memberships.GetMembershipBy(func(membership model.GroupMembership) bool { - return membership.GroupID == group.ID - }) - if group.CurrentMember != nil { - list[index] = group + offset := int64(0) + if groupsFilter.Offset != nil { + offset = *groupsFilter.Offset + } + + pipeline := bson.A{ + bson.D{{Key: "$match", Value: filter}}, + bson.D{{Key: "$addFields", Value: bson.M{"_c_title": bson.M{"$toUpper": "$title"}}}}, + bson.D{{Key: "$sort", Value: aggrSort}}, + bson.D{{Key: "$skip", Value: offset}}, + } + if groupsFilter.Limit != nil { + if limitIDRowNumber > 0 { + pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(limitIDRowNumber)}}) + } else { + pipeline = append(pipeline, bson.D{{Key: "$limit", Value: *groupsFilter.Limit}}) } } + + count, err = sa.db.groups.CountDocumentsWithContext(ctx, filter) + if err != nil { + return err + } + + var memberships model.MembershipCollection + + // + //err = sa.db.groups.FindWithContext(ctx, filter, &list, findOptions) + err = sa.db.groups.AggregateWithContext(ctx, pipeline, &list, nil) + if err != nil { + return err + } + + if userID != nil { + for index, group := range list { + group.CurrentMember = memberships.GetMembershipBy(func(membership model.GroupMembership) bool { + return membership.GroupID == group.ID + }) + if group.CurrentMember != nil { + list[index] = group + } + } + } + + return nil + }) + if err != nil { + return 0, nil, err } return count, list, nil } -func (sa *Adapter) buildMainQuery(userID *string, clientID string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) (bson.D, error) { - var err error +func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, clientID string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) (bson.D, error) { + groupIDs := []string{} - var memberships model.MembershipCollection - if userID != nil { + groupIDMap := map[string]bool{} + if len(groupsFilter.GroupIDs) > 0 { + for _, groupID := range groupsFilter.GroupIDs { + groupIDs = append(groupIDs, groupID) + groupIDMap[groupID] = true + } + } + + // Credits to Ryan Oberlander suggest + if userID != nil || groupsFilter.MemberID != nil || groupsFilter.MemberExternalID != nil { // find group memberships - memberships, err = sa.FindUserGroupMemberships(clientID, *userID) + memberships, err := sa.FindGroupMembershipsWithContext(context, clientID, model.MembershipFilter{ + ID: groupsFilter.MemberID, + UserID: userID, + ExternalID: groupsFilter.MemberExternalID, + Statuses: groupsFilter.MemberStatuses, + }) if err != nil { return nil, err } for _, membership := range memberships.Items { - groupIDs = append(groupIDs, membership.GroupID) + if len(groupIDMap) == 0 || !groupIDMap[membership.GroupID] { + groupIDs = append(groupIDs, membership.GroupID) + groupIDMap[membership.GroupID] = true + } } } @@ -826,16 +901,6 @@ func (sa *Adapter) buildMainQuery(userID *string, clientID string, groupsFilter } } - findOptions := options.Find() - if groupsFilter.Order != nil && "desc" == *groupsFilter.Order { - findOptions.SetSort(bson.D{ - {Key: "title", Value: -1}, - }) - } else { - findOptions.SetSort(bson.D{ - {Key: "title", Value: 1}, - }) - } if groupsFilter.DaysInactive != nil { pastTime := time.Now().Add(time.Duration(*groupsFilter.DaysInactive) * -24 * time.Hour) filter = append(filter, primitive.E{Key: "date_updated", Value: bson.M{"$lt": pastTime}}) diff --git a/driven/storage/adapter_v3.go b/driven/storage/adapter_v3.go index cb5f1c17..94a669b1 100644 --- a/driven/storage/adapter_v3.go +++ b/driven/storage/adapter_v3.go @@ -23,7 +23,7 @@ func (sa *Adapter) FindGroupsV3(context TransactionContext, clientID string, fil var memberships model.MembershipCollection findOptions := options.Find() - groupFilter, err := sa.buildGroupsFilter(clientID, context, filter) + groupFilter, err := sa.buildMainQuery(context, nil, clientID, filter, true) if err != nil { return nil, err } @@ -56,7 +56,7 @@ func (sa *Adapter) FindGroupsV3(context TransactionContext, clientID string, fil return list, nil } -func (sa *Adapter) buildGroupsFilter(clientID string, context TransactionContext, filter model.GroupsFilter) (bson.D, error) { +func (sa *Adapter) buildGroupsFilter11(clientID string, context TransactionContext, filter model.GroupsFilter) (bson.D, error) { var groupIDs []string groupFilter := bson.D{primitive.E{Key: "client_id", Value: clientID}} @@ -172,7 +172,7 @@ func (sa *Adapter) buildGroupsFilter(clientID string, context TransactionContext func (sa *Adapter) CalculateGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter) (*model.StatsResult, error) { var result *model.StatsResult err := sa.PerformTransaction(func(ctx TransactionContext) error { - baseFilter, err := sa.buildGroupsFilter(clientID, ctx, filter.BaseFilter) + baseFilter, err := sa.buildMainQuery(ctx, ¤t.ID, clientID, filter.BaseFilter, false) if err != nil { return err } @@ -182,7 +182,7 @@ func (sa *Adapter) CalculateGroupFilterStats(clientID string, current *model.Use subFilters := bson.D{} for key, value := range filter.SubFilters { innerSubFilter := bson.A{} - filter, err := sa.buildGroupsFilter(clientID, ctx, value) + filter, err := sa.buildMainQuery(ctx, ¤t.ID, clientID, value, false) if err != nil { return err } diff --git a/go.mod b/go.mod index 5e71c1c6..ac127956 100644 --- a/go.mod +++ b/go.mod @@ -1,39 +1,37 @@ module groups -go 1.24.2 - -toolchain go1.24.3 +go 1.24.8 require ( github.com/casbin/casbin v1.9.1 - github.com/coreos/go-oidc/v3 v3.14.1 + github.com/coreos/go-oidc/v3 v3.17.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/robfig/cron/v3 v3.0.1 - github.com/rokwire/rokwire-building-block-sdk-go v1.8.3 + github.com/rokwire/rokwire-building-block-sdk-go v1.8.4 github.com/swaggo/http-swagger v1.3.4 - github.com/swaggo/swag v1.16.4 - go.mongodb.org/mongo-driver v1.17.4 - golang.org/x/sync v0.15.0 + github.com/swaggo/swag v1.16.6 + go.mongodb.org/mongo-driver v1.17.6 + golang.org/x/sync v0.19.0 gopkg.in/go-playground/validator.v9 v9.31.0 ) require ( github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect - github.com/casbin/casbin/v2 v2.104.0 // indirect - github.com/casbin/govaluate v1.3.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.121.0 // indirect + github.com/casbin/govaluate v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-openapi/jsonpointer v0.21.2 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -46,12 +44,13 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/net v0.39.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.36.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c211bbef..236ee75c 100644 --- a/go.sum +++ b/go.sum @@ -3,25 +3,26 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= -github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/casbin/casbin v1.9.1 h1:ucjbS5zTrmSLtH4XogqOG920Poe6QatdXtz1FEbApeM= github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog= -github.com/casbin/casbin/v2 v2.104.0 h1:qDakyBZ4jUg1VskF1+UzIwkg+uXWcp0u0M9PMm1RsTA= -github.com/casbin/casbin/v2 v2.104.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= -github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/casbin/v2 v2.121.0 h1:lrgTnLJTsdpe8Kdgi+NedM9+K7ftYBaK19OE+IZUwdk= +github.com/casbin/casbin/v2 v2.121.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= -github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= -github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/casbin/govaluate v1.9.0 h1:XB53bSw+gaQ7tjTlFJsuTThPCQBxyUeQZ3drsKiicEY= +github.com/casbin/govaluate v1.9.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= +github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= @@ -34,10 +35,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -68,22 +69,22 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rokwire/rokwire-building-block-sdk-go v1.8.3 h1:QmCGeVBFZ655yrmVzEpb6PbAtLywiais01oaAkxSVGQ= -github.com/rokwire/rokwire-building-block-sdk-go v1.8.3/go.mod h1:0Nw2kjCxItS/Wm9JIDeiz23dxT1H2m3SisBASmLhXb4= +github.com/rokwire/rokwire-building-block-sdk-go v1.8.4 h1:UrDSzgTb92CceSQoyFNJB2AhHV6x4VVtyqoh0vjb/UI= +github.com/rokwire/rokwire-building-block-sdk-go v1.8.4/go.mod h1:Jeoark9GuqG9jjyUq1RwaskwhnlmPKY0SWz9JeFiOO4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= -github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= -github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -93,28 +94,28 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= -go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -122,8 +123,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -131,14 +132,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From df564893fae1ac2cecbf55cb956975a780365c42 Mon Sep 17 00:00:00 2001 From: Mladen Date: Fri, 12 Dec 2025 20:00:04 +0200 Subject: [PATCH 3/8] Additional fixes for membership status filter --- driven/storage/adapter.go | 20 ++++++++++++-------- driven/storage/adapter_v3.go | 7 +++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 9e634aab..d7b7a39d 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -652,7 +652,7 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode var count int64 var list []model.Group err := sa.PerformTransaction(func(ctx TransactionContext) error { - filter, err := sa.buildMainQuery(ctx, userID, clientID, groupsFilter, skipMembershipCheck) + filter, memberships, err := sa.buildMainQuery(ctx, userID, clientID, groupsFilter, skipMembershipCheck) if err != nil { return err } @@ -724,8 +724,6 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode return err } - var memberships model.MembershipCollection - // //err = sa.db.groups.FindWithContext(ctx, filter, &list, findOptions) err = sa.db.groups.AggregateWithContext(ctx, pipeline, &list, nil) @@ -753,7 +751,7 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode return count, list, nil } -func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, clientID string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) (bson.D, error) { +func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, clientID string, groupsFilter model.GroupsFilter, skipMembershipCheck bool) (bson.D, model.MembershipCollection, error) { groupIDs := []string{} groupIDMap := map[string]bool{} @@ -764,17 +762,20 @@ func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, cl } } + var memberships model.MembershipCollection + // Credits to Ryan Oberlander suggest if userID != nil || groupsFilter.MemberID != nil || groupsFilter.MemberExternalID != nil { // find group memberships - memberships, err := sa.FindGroupMembershipsWithContext(context, clientID, model.MembershipFilter{ + var err error + memberships, err = sa.FindGroupMembershipsWithContext(context, clientID, model.MembershipFilter{ ID: groupsFilter.MemberID, UserID: userID, ExternalID: groupsFilter.MemberExternalID, Statuses: groupsFilter.MemberStatuses, }) if err != nil { - return nil, err + return nil, model.MembershipCollection{}, err } for _, membership := range memberships.Items { @@ -785,7 +786,10 @@ func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, cl } } - filter := bson.D{primitive.E{Key: "client_id", Value: clientID}} + filter := bson.D{} + if len(groupsFilter.MemberStatuses) > 0 { + filter = append(filter, bson.E{Key: "_id", Value: bson.M{"$in": groupIDs}}) + } if groupsFilter.GroupIDs != nil { filter = append(filter, bson.E{Key: "_id", Value: bson.M{"$in": groupsFilter.GroupIDs}}) } @@ -905,7 +909,7 @@ func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, cl pastTime := time.Now().Add(time.Duration(*groupsFilter.DaysInactive) * -24 * time.Hour) filter = append(filter, primitive.E{Key: "date_updated", Value: bson.M{"$lt": pastTime}}) } - return filter, nil + return filter, memberships, nil } // FindGroupByID finds one groups by ID and clientID diff --git a/driven/storage/adapter_v3.go b/driven/storage/adapter_v3.go index 94a669b1..5bf04ae2 100644 --- a/driven/storage/adapter_v3.go +++ b/driven/storage/adapter_v3.go @@ -20,10 +20,9 @@ import ( func (sa *Adapter) FindGroupsV3(context TransactionContext, clientID string, filter model.GroupsFilter) ([]model.Group, error) { var err error - var memberships model.MembershipCollection findOptions := options.Find() - groupFilter, err := sa.buildMainQuery(context, nil, clientID, filter, true) + groupFilter, memberships, err := sa.buildMainQuery(context, nil, clientID, filter, true) if err != nil { return nil, err } @@ -172,7 +171,7 @@ func (sa *Adapter) buildGroupsFilter11(clientID string, context TransactionConte func (sa *Adapter) CalculateGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter) (*model.StatsResult, error) { var result *model.StatsResult err := sa.PerformTransaction(func(ctx TransactionContext) error { - baseFilter, err := sa.buildMainQuery(ctx, ¤t.ID, clientID, filter.BaseFilter, false) + baseFilter, _, err := sa.buildMainQuery(ctx, ¤t.ID, clientID, filter.BaseFilter, false) if err != nil { return err } @@ -182,7 +181,7 @@ func (sa *Adapter) CalculateGroupFilterStats(clientID string, current *model.Use subFilters := bson.D{} for key, value := range filter.SubFilters { innerSubFilter := bson.A{} - filter, err := sa.buildMainQuery(ctx, ¤t.ID, clientID, value, false) + filter, _, err := sa.buildMainQuery(ctx, ¤t.ID, clientID, value, false) if err != nil { return err } From 9796dfa526918c573fe1e4b2bbff1ba57644623c Mon Sep 17 00:00:00 2001 From: Mladen Date: Mon, 15 Dec 2025 15:34:17 +0200 Subject: [PATCH 4/8] Fix docs --- docs/docs.go | 141 ++++++++++++++++++++++++++++++- docs/swagger.json | 141 ++++++++++++++++++++++++++++++- docs/swagger.yaml | 102 +++++++++++++++++++++- driver/web/rest/admin_apis_v2.go | 2 +- driver/web/rest/apis_v2.go | 4 +- go.mod | 2 +- 6 files changed, 379 insertions(+), 13 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 1ed7b838..1e6d8ec7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1516,15 +1516,64 @@ const docTemplate = `{ "AppUserAuth": [] } ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", + "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", "consumes": [ "application/json" ], "tags": [ "Admin" ], - "operationId": "AdminGetGroupsV3", + "operationId": "AdminGetGroupsV2", "parameters": [ + { + "type": "string", + "description": "APP", + "name": "APP", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! category - filter by category", + "name": "category", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! privacy - filter by privacy", + "name": "privacy", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! offset - skip number of records", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! limit - limit the result", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - Filter by number of days inactive", + "name": "days_inactive", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false.", + "name": "include_hidden", + "in": "query" + }, { "description": "body data", "name": "data", @@ -1539,7 +1588,10 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/getGroupsResponseV3" + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } } } } @@ -1754,6 +1806,42 @@ const docTemplate = `{ } } }, + "/api/admin/v3/groups/load": { + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", + "consumes": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "operationId": "AdminGetGroupsV3", + "parameters": [ + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/getGroupsResponseV3" + } + } + } + } + }, "/api/analytics/groups": { "get": { "security": [ @@ -4850,6 +4938,42 @@ const docTemplate = `{ } } }, + "/api/v3/groups/load": { + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsV3", + "parameters": [ + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/getGroupsResponseV3" + } + } + } + } + }, "/authman/synchronize": { "post": { "security": [ @@ -5413,6 +5537,10 @@ const docTemplate = `{ "description": "result limit", "type": "integer" }, + "limit_id": { + "description": "limit id", + "type": "string" + }, "member_external_id": { "description": "member user external id", "type": "string" @@ -5421,6 +5549,13 @@ const docTemplate = `{ "description": "member id", "type": "string" }, + "member_statuses": { + "description": "member user status", + "type": "array", + "items": { + "type": "string" + } + }, "member_user_id": { "description": "member user id", "type": "string" diff --git a/docs/swagger.json b/docs/swagger.json index eb2fff96..28dd85b4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1513,15 +1513,64 @@ "AppUserAuth": [] } ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", + "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", "consumes": [ "application/json" ], "tags": [ "Admin" ], - "operationId": "AdminGetGroupsV3", + "operationId": "AdminGetGroupsV2", "parameters": [ + { + "type": "string", + "description": "APP", + "name": "APP", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! category - filter by category", + "name": "category", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! privacy - filter by privacy", + "name": "privacy", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! offset - skip number of records", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! limit - limit the result", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - Filter by number of days inactive", + "name": "days_inactive", + "in": "query" + }, + { + "type": "string", + "description": "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false.", + "name": "include_hidden", + "in": "query" + }, { "description": "body data", "name": "data", @@ -1536,7 +1585,10 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/getGroupsResponseV3" + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } } } } @@ -1751,6 +1803,42 @@ } } }, + "/api/admin/v3/groups/load": { + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", + "consumes": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "operationId": "AdminGetGroupsV3", + "parameters": [ + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/getGroupsResponseV3" + } + } + } + } + }, "/api/analytics/groups": { "get": { "security": [ @@ -4847,6 +4935,42 @@ } } }, + "/api/v3/groups/load": { + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V3", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsV3", + "parameters": [ + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/getGroupsResponseV3" + } + } + } + } + }, "/authman/synchronize": { "post": { "security": [ @@ -5410,6 +5534,10 @@ "description": "result limit", "type": "integer" }, + "limit_id": { + "description": "limit id", + "type": "string" + }, "member_external_id": { "description": "member user external id", "type": "string" @@ -5418,6 +5546,13 @@ "description": "member id", "type": "string" }, + "member_statuses": { + "description": "member user status", + "type": "array", + "items": { + "type": "string" + } + }, "member_user_id": { "description": "member user id", "type": "string" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 08db382f..f7bc4406 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -284,12 +284,20 @@ definitions: limit: description: result limit type: integer + limit_id: + description: limit id + type: string member_external_id: description: member user external id type: string member_id: description: member id type: string + member_statuses: + description: member user status + items: + type: string + type: array member_user_id: description: member user id type: string @@ -2190,9 +2198,49 @@ paths: consumes: - application/json description: Gives the groups list. It can be filtered by category, title and - privacy. V3 - operationId: AdminGetGroupsV3 + privacy. V2 + operationId: AdminGetGroupsV2 parameters: + - description: APP + in: header + name: APP + required: true + type: string + - description: Deprecated - instead use request body filter! Filtering by group's + title (case-insensitive) + in: query + name: title + type: string + - description: Deprecated - instead use request body filter! category - filter + by category + in: query + name: category + type: string + - description: Deprecated - instead use request body filter! privacy - filter + by privacy + in: query + name: privacy + type: string + - description: Deprecated - instead use request body filter! offset - skip number + of records + in: query + name: offset + type: string + - description: Deprecated - instead use request body filter! limit - limit the + result + in: query + name: limit + type: string + - description: Deprecated - Filter by number of days inactive + in: query + name: days_inactive + type: string + - description: Deprecated - instead use request body filter! include_hidden + - Includes hidden groups if a search by title is performed. Possible value + is true. Default false. + in: query + name: include_hidden + type: string - description: body data in: body name: data @@ -2203,7 +2251,9 @@ paths: "200": description: OK schema: - $ref: '#/definitions/getGroupsResponseV3' + items: + $ref: '#/definitions/Group' + type: array security: - AppUserAuth: [] tags: @@ -2357,6 +2407,29 @@ paths: - APIKeyAuth: [] tags: - Admin + /api/admin/v3/groups/load: + post: + consumes: + - application/json + description: Gives the groups list. It can be filtered by category, title and + privacy. V3 + operationId: AdminGetGroupsV3 + parameters: + - description: body data + in: body + name: data + required: true + schema: + $ref: '#/definitions/GroupsFilter' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/getGroupsResponseV3' + security: + - AppUserAuth: [] + tags: + - Admin /api/analytics/groups: get: consumes: @@ -4373,6 +4446,29 @@ paths: - AppUserAuth: [] tags: - Client + /api/v3/groups/load: + post: + consumes: + - application/json + description: Gives the groups list. It can be filtered by category, title and + privacy. V3 + operationId: GetGroupsV3 + parameters: + - description: body data + in: body + name: data + required: true + schema: + $ref: '#/definitions/GroupsFilter' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/getGroupsResponseV3' + security: + - AppUserAuth: [] + tags: + - Client /authman/synchronize: post: consumes: diff --git a/driver/web/rest/admin_apis_v2.go b/driver/web/rest/admin_apis_v2.go index a1b0b906..a0a3d473 100644 --- a/driver/web/rest/admin_apis_v2.go +++ b/driver/web/rest/admin_apis_v2.go @@ -155,7 +155,7 @@ type getGroupsResponseV3 struct { // @Param data body model.GroupsFilter true "body data" // @Success 200 {object} getGroupsResponseV3 // @Security AppUserAuth -// @Router /api/admin/v2/groups [post] +// @Router /api/admin/v3/groups/load [post] func (h *AdminApisHandler) GetGroupsV3(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { var groupsFilter model.GroupsFilter diff --git a/driver/web/rest/apis_v2.go b/driver/web/rest/apis_v2.go index 66186b6c..17af63d0 100644 --- a/driver/web/rest/apis_v2.go +++ b/driver/web/rest/apis_v2.go @@ -137,12 +137,12 @@ func (h *ApisHandler) GetGroupsV2(clientID string, current *model.User, w http.R // GetGroupsV3 gets groups. It can be filtered by category, title and privacy. V3 // @Description Gives the groups list. It can be filtered by category, title and privacy. V3 // @ID GetGroupsV3 -// @Tags Admin +// @Tags Client // @Accept json // @Param data body model.GroupsFilter true "body data" // @Success 200 {object} getGroupsResponseV3 // @Security AppUserAuth -// @Router /api/admin/v2/groups [post] +// @Router /api/v3/groups/load [post] func (h *ApisHandler) GetGroupsV3(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { var groupsFilter model.GroupsFilter diff --git a/go.mod b/go.mod index ac127956..a4c32431 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module groups -go 1.24.8 +go 1.24.6 require ( github.com/casbin/casbin v1.9.1 From 7fcc5e3b9407242d151d5ecc4f8eba70575b150f Mon Sep 17 00:00:00 2001 From: Mladen Date: Mon, 15 Dec 2025 15:43:01 +0200 Subject: [PATCH 5/8] Additonal api doc fix --- docs/docs.go | 51 +++++++++++++++++++++++++++++++++++ docs/swagger.json | 51 +++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 33 +++++++++++++++++++++++ driver/web/rest/admin_apis.go | 2 +- 4 files changed, 136 insertions(+), 1 deletion(-) diff --git a/docs/docs.go b/docs/docs.go index 1e6d8ec7..bc9535dd 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -406,6 +406,57 @@ const docTemplate = `{ } } } + }, + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Updates a membership. Only the status can be changed.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "operationId": "AdminCreateMemberships", + "parameters": [ + { + "type": "string", + "description": "APP", + "name": "APP", + "in": "header", + "required": true + }, + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/MembershipStatus" + } + } + }, + { + "type": "string", + "description": "Group ID", + "name": "group-id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } } }, "/api/admin/group/{group-id}/members/v2": { diff --git a/docs/swagger.json b/docs/swagger.json index 28dd85b4..75545ea9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -403,6 +403,57 @@ } } } + }, + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Updates a membership. Only the status can be changed.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "operationId": "AdminCreateMemberships", + "parameters": [ + { + "type": "string", + "description": "APP", + "name": "APP", + "in": "header", + "required": true + }, + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/MembershipStatus" + } + } + }, + { + "type": "string", + "description": "Group ID", + "name": "group-id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } } }, "/api/admin/group/{group-id}/members/v2": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f7bc4406..cc899c38 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1445,6 +1445,39 @@ paths: - AppUserAuth: [] tags: - Admin + post: + consumes: + - application/json + description: Updates a membership. Only the status can be changed. + operationId: AdminCreateMemberships + parameters: + - description: APP + in: header + name: APP + required: true + type: string + - description: body data + in: body + name: data + required: true + schema: + items: + $ref: '#/definitions/MembershipStatus' + type: array + - description: Group ID + in: path + name: group-id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + security: + - AppUserAuth: [] + tags: + - Admin /api/admin/group/{group-id}/members/v2: post: consumes: diff --git a/driver/web/rest/admin_apis.go b/driver/web/rest/admin_apis.go index 34b4b264..8db6efae 100644 --- a/driver/web/rest/admin_apis.go +++ b/driver/web/rest/admin_apis.go @@ -672,7 +672,7 @@ type adminCreateMembershipsRequest []model.MembershipStatus // @Param group-id path string true "Group ID" // @Success 200 // @Security AppUserAuth -// @Router /group/{group-id}/members [post] +// @Router /api/admin/group/{group-id}/members [post] func (h *AdminApisHandler) CreateMemberships(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { //validate input params := mux.Vars(r) From e551b03274950e3101664b5c9192614ed3c1a8fb Mon Sep 17 00:00:00 2001 From: Mladen Date: Mon, 15 Dec 2025 15:54:16 +0200 Subject: [PATCH 6/8] Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6513cfd..8490e2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased + +## [1.73.0] - 2025-12-15 +### Added +- USABILITY UI CleanUp: Group Filters - API improvements [#613](https://github.com/rokwire/groups-building-block/issues/613) + ## [1.72.0] - 2025-09-15 ### Added - Change participant age field for participant filtering in research groups feature [#604](https://github.com/rokwire/groups-building-block/issues/604) From d4754623d229dd6de88be9f8ccd0fbda8fe2021b Mon Sep 17 00:00:00 2001 From: Mladen Date: Mon, 15 Dec 2025 16:27:41 +0200 Subject: [PATCH 7/8] Fix array out of bound --- driven/storage/adapter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index d7b7a39d..9204d936 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -695,7 +695,7 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode return err } - if rowNumbers[0].RowNumber != 0 { + if len(rowNumbers) > 0 && rowNumbers[0].RowNumber != 0 { limitIDRowNumber = rowNumbers[0].RowNumber } } From c1d7f2de1ea9dd3e839f8085290f23fa84260418 Mon Sep 17 00:00:00 2001 From: Mladen Date: Mon, 15 Dec 2025 16:29:14 +0200 Subject: [PATCH 8/8] Changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8490e2ad..36b4e5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [1.73.1] - 2025-12-15 +### Fixed +- SABILITY UI CleanUp: Group Filters - API improvements. Fix bed array handling[#613](https://github.com/rokwire/groups-building-block/issues/613) ## [1.73.0] - 2025-12-15 ### Added