From 44d0275864b10ce7b8f87c1be1f02017ddf10804 Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 10 Dec 2025 15:32:23 +0200 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] 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 08/22] 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 From 18b498d5c168a07ecf1e1a303acc44c14f43ed02 Mon Sep 17 00:00:00 2001 From: Mladen Date: Mon, 15 Dec 2025 17:37:27 +0200 Subject: [PATCH 09/22] Fix docs --- {docs => driver/docs}/docs.go | 0 {docs => driver/docs}/swagger.json | 0 {docs => driver/docs}/swagger.yaml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {docs => driver/docs}/docs.go (100%) rename {docs => driver/docs}/swagger.json (100%) rename {docs => driver/docs}/swagger.yaml (100%) diff --git a/docs/docs.go b/driver/docs/docs.go similarity index 100% rename from docs/docs.go rename to driver/docs/docs.go diff --git a/docs/swagger.json b/driver/docs/swagger.json similarity index 100% rename from docs/swagger.json rename to driver/docs/swagger.json diff --git a/docs/swagger.yaml b/driver/docs/swagger.yaml similarity index 100% rename from docs/swagger.yaml rename to driver/docs/swagger.yaml From 0b65242a4ce3be4e9d8affaea648fa02c71a3d36 Mon Sep 17 00:00:00 2001 From: Mladen Date: Mon, 15 Dec 2025 17:38:26 +0200 Subject: [PATCH 10/22] Changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b4e5f4..eacfe7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [1.73.2] - 2025-12-15 +### Fixed +- SABILITY UI CleanUp: Group Filters - API improvements. Fix api doc [#613](https://github.com/rokwire/groups-building-block/issues/613) + ## [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) +- 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 From da94fdfa1bb302fb00922fc204a5fe60209aebbb Mon Sep 17 00:00:00 2001 From: Mladen Date: Tue, 16 Dec 2025 12:49:29 +0200 Subject: [PATCH 11/22] Redo api doc --- {driver/docs => docs}/docs.go | 0 {driver/docs => docs}/swagger.json | 0 {driver/docs => docs}/swagger.yaml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {driver/docs => docs}/docs.go (100%) rename {driver/docs => docs}/swagger.json (100%) rename {driver/docs => docs}/swagger.yaml (100%) diff --git a/driver/docs/docs.go b/docs/docs.go similarity index 100% rename from driver/docs/docs.go rename to docs/docs.go diff --git a/driver/docs/swagger.json b/docs/swagger.json similarity index 100% rename from driver/docs/swagger.json rename to docs/swagger.json diff --git a/driver/docs/swagger.yaml b/docs/swagger.yaml similarity index 100% rename from driver/docs/swagger.yaml rename to docs/swagger.yaml From 6e4eaf92919ffa49f39355c36e8d2d8c7d8fffd6 Mon Sep 17 00:00:00 2001 From: Mladen Date: Tue, 16 Dec 2025 12:55:54 +0200 Subject: [PATCH 12/22] Fix build and update libraries and go version --- Dockerfile | 4 ++-- go.mod | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5277733a..0f1762c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24-bullseye AS builder +FROM golang:1.25-bookworm AS builder ENV CGO_ENABLED=0 @@ -9,7 +9,7 @@ WORKDIR /groups-app COPY . . RUN make -FROM alpine:3.22 +FROM alpine:3.23 #we need timezone database + certificates RUN apk add --no-cache make tzdata ca-certificates diff --git a/go.mod b/go.mod index a4c32431..b96e3766 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module groups -go 1.24.6 +go 1.25.5 require ( github.com/casbin/casbin v1.9.1 From ebf3eeeaf9f24beb9a39a8b487706596980dc8d2 Mon Sep 17 00:00:00 2001 From: Mladen Date: Tue, 16 Dec 2025 13:34:20 +0200 Subject: [PATCH 13/22] Update apis and regenerate docs --- docs/docs.go | 244 ++----- docs/swagger.json | 244 ++----- docs/swagger.yaml | 175 +---- driver/web/adapter.go | 2 +- driver/web/rest/apis.go | 782 +--------------------- driver/web/rest/apis_groups.go | 1118 ++++++++++++++++++++++++++++++++ driver/web/rest/apis_v2.go | 339 ---------- 7 files changed, 1231 insertions(+), 1673 deletions(-) create mode 100644 driver/web/rest/apis_groups.go delete mode 100644 driver/web/rest/apis_v2.go diff --git a/docs/docs.go b/docs/docs.go index bc9535dd..8389dfd1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2302,46 +2302,6 @@ const docTemplate = `{ } } }, - "/api/group/stats": { - "post": { - "security": [ - { - "AppUserAuth": [] - } - ], - "description": "Gets groups filter stats", - "consumes": [ - "application/json" - ], - "tags": [ - "Client" - ], - "operationId": "GetGroupsFilterStats", - "parameters": [ - { - "type": "string", - "description": "APP", - "name": "APP", - "in": "header", - "required": true - }, - { - "description": "body data", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/StatsFilter" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, "/api/group/{group-id}/authman/synchronize": { "post": { "security": [ @@ -4529,7 +4489,7 @@ const docTemplate = `{ "APIKeyAuth": [] } ], - "description": "Logs in the user and refactor the user record and linked data if need", + "description": "Deprecated: Don't use it! Logs in the user and refactor the user record and linked data if need", "tags": [ "Client" ], @@ -4572,168 +4532,6 @@ const docTemplate = `{ } } }, - "/api/v2/groups": { - "get": { - "security": [ - { - "AppUserAuth": [] - } - ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", - "consumes": [ - "application/json" - ], - "tags": [ - "Client" - ], - "operationId": "GetGroupsV2", - "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 - 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", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupsFilter" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Group" - } - } - } - } - }, - "post": { - "security": [ - { - "AppUserAuth": [] - } - ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", - "consumes": [ - "application/json" - ], - "tags": [ - "Client" - ], - "operationId": "GetGroupsV2", - "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 - 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", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupsFilter" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Group" - } - } - } - } - } - }, "/api/v2/groups/{id}": { "get": { "security": [ @@ -5025,6 +4823,46 @@ const docTemplate = `{ } } }, + "/api/v3/groups/stats": { + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gets groups filter stats", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsFilterStatsV3", + "parameters": [ + { + "type": "string", + "description": "APP", + "name": "APP", + "in": "header", + "required": true + }, + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/StatsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/authman/synchronize": { "post": { "security": [ diff --git a/docs/swagger.json b/docs/swagger.json index 75545ea9..1dda218d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2299,46 +2299,6 @@ } } }, - "/api/group/stats": { - "post": { - "security": [ - { - "AppUserAuth": [] - } - ], - "description": "Gets groups filter stats", - "consumes": [ - "application/json" - ], - "tags": [ - "Client" - ], - "operationId": "GetGroupsFilterStats", - "parameters": [ - { - "type": "string", - "description": "APP", - "name": "APP", - "in": "header", - "required": true - }, - { - "description": "body data", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/StatsFilter" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, "/api/group/{group-id}/authman/synchronize": { "post": { "security": [ @@ -4526,7 +4486,7 @@ "APIKeyAuth": [] } ], - "description": "Logs in the user and refactor the user record and linked data if need", + "description": "Deprecated: Don't use it! Logs in the user and refactor the user record and linked data if need", "tags": [ "Client" ], @@ -4569,168 +4529,6 @@ } } }, - "/api/v2/groups": { - "get": { - "security": [ - { - "AppUserAuth": [] - } - ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", - "consumes": [ - "application/json" - ], - "tags": [ - "Client" - ], - "operationId": "GetGroupsV2", - "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 - 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", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupsFilter" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Group" - } - } - } - } - }, - "post": { - "security": [ - { - "AppUserAuth": [] - } - ], - "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", - "consumes": [ - "application/json" - ], - "tags": [ - "Client" - ], - "operationId": "GetGroupsV2", - "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 - 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", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupsFilter" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Group" - } - } - } - } - } - }, "/api/v2/groups/{id}": { "get": { "security": [ @@ -5022,6 +4820,46 @@ } } }, + "/api/v3/groups/stats": { + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gets groups filter stats", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsFilterStatsV3", + "parameters": [ + { + "type": "string", + "description": "APP", + "name": "APP", + "in": "header", + "required": true + }, + { + "description": "body data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/StatsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/authman/synchronize": { "post": { "security": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cc899c38..7022e8c9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3487,31 +3487,6 @@ paths: - AppUserAuth: [] tags: - Client - /api/group/stats: - post: - consumes: - - application/json - description: Gets groups filter stats - operationId: GetGroupsFilterStats - parameters: - - description: APP - in: header - name: APP - required: true - type: string - - description: body data - in: body - name: data - required: true - schema: - $ref: '#/definitions/StatsFilter' - responses: - "200": - description: OK - security: - - AppUserAuth: [] - tags: - - Client /api/groups: get: consumes: @@ -4149,8 +4124,8 @@ paths: - Client /api/user/login: get: - description: Logs in the user and refactor the user record and linked data if - need + description: 'Deprecated: Don''t use it! Logs in the user and refactor the user + record and linked data if need' operationId: LoginUser responses: "200": @@ -4179,127 +4154,6 @@ paths: - AppUserAuth: [] tags: - Client - /api/v2/groups: - get: - consumes: - - application/json - description: Gives the groups list. It can be filtered by category, title and - privacy. V2 - operationId: GetGroupsV2 - 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 - 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 - required: true - schema: - $ref: '#/definitions/GroupsFilter' - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/Group' - type: array - security: - - AppUserAuth: [] - tags: - - Client - post: - consumes: - - application/json - description: Gives the groups list. It can be filtered by category, title and - privacy. V2 - operationId: GetGroupsV2 - 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 - 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 - required: true - schema: - $ref: '#/definitions/GroupsFilter' - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/Group' - type: array - security: - - AppUserAuth: [] - tags: - - Client /api/v2/groups/{id}: get: consumes: @@ -4502,6 +4356,31 @@ paths: - AppUserAuth: [] tags: - Client + /api/v3/groups/stats: + post: + consumes: + - application/json + description: Gets groups filter stats + operationId: GetGroupsFilterStatsV3 + parameters: + - description: APP + in: header + name: APP + required: true + type: string + - description: body data + in: body + name: data + required: true + schema: + $ref: '#/definitions/StatsFilter' + responses: + "200": + description: OK + security: + - AppUserAuth: [] + tags: + - Client /authman/synchronize: post: consumes: diff --git a/driver/web/adapter.go b/driver/web/adapter.go index 0449e042..48608d4f 100644 --- a/driver/web/adapter.go +++ b/driver/web/adapter.go @@ -144,6 +144,7 @@ func (we *Adapter) Start() { restSubrouter.HandleFunc("/v3/group", we.idTokenAuthWrapFunc(we.apisHandler.CreateGroupV3)).Methods("POST") restSubrouter.HandleFunc("/v3/groups/load", we.anonymousAuthWrapFunc(we.apisHandler.GetGroupsV3)).Methods("POST") + restSubrouter.HandleFunc("/v3/groups/stats", we.idTokenAuthWrapFunc(we.apisHandler.GetGroupsFilterStatsV3)).Methods("POST") //V1 Client APIs restSubrouter.HandleFunc("/authman/synchronize", we.idTokenAuthWrapFunc(we.apisHandler.SynchronizeAuthman)).Methods("POST") @@ -158,7 +159,6 @@ func (we *Adapter) Start() { restSubrouter.HandleFunc("/group/{id}/stats", we.anonymousAuthWrapFunc(we.apisHandler.GetGroupStats)).Methods("GET") restSubrouter.HandleFunc("/group/{id}/report/abuse", we.idTokenAuthWrapFunc(we.apisHandler.ReportAbuseGroup)).Methods("PUT") restSubrouter.HandleFunc("/group/{id}", we.idTokenAuthWrapFunc(we.apisHandler.DeleteGroup)).Methods("DELETE") - restSubrouter.HandleFunc("/group/stats", we.idTokenAuthWrapFunc(we.apisHandler.GetGroupsFilterStats)).Methods("POST") restSubrouter.HandleFunc("/group/{group-id}/pending-members", we.idTokenAuthWrapFunc(we.apisHandler.CreatePendingMember)).Methods("POST") restSubrouter.HandleFunc("/group/{group-id}/pending-members", we.idTokenAuthWrapFunc(we.apisHandler.DeletePendingMember)).Methods("DELETE") diff --git a/driver/web/rest/apis.go b/driver/web/rest/apis.go index d8860a14..aead09c9 100644 --- a/driver/web/rest/apis.go +++ b/driver/web/rest/apis.go @@ -19,12 +19,11 @@ import ( "fmt" "groups/core" "groups/core/model" - "groups/utils" + "io" "log" "net/http" "strconv" - "strings" "time" "github.com/gorilla/mux" @@ -68,728 +67,8 @@ func (h ApisHandler) Version(log *logs.Log, req *http.Request, user *model.User) return log.HTTPResponseSuccessMessage(h.app.Services.GetVersion()) } -type createGroupRequest struct { - Title string `json:"title" validate:"required"` - Description *string `json:"description"` - Category string `json:"category"` - Tags []string `json:"tags"` - Privacy string `json:"privacy" validate:"required,oneof=public private"` - Hidden bool `json:"hidden_for_search"` - CreatorName string `json:"creator_name"` - CreatorEmail string `json:"creator_email"` - CreatorPhotoURL string `json:"creator_photo_url"` - ImageURL *string `json:"image_url"` - WebURL *string `json:"web_url"` - MembershipQuestions []string `json:"membership_questions"` - AuthmanEnabled bool `json:"authman_enabled"` - AuthmanGroup *string `json:"authman_group"` - OnlyAdminsCanCreatePolls bool `json:"only_admins_can_create_polls" ` - CanJoinAutomatically bool `json:"can_join_automatically"` - AttendanceGroup bool `json:"attendance_group" ` - ResearchOpen bool `json:"research_open"` - ResearchGroup bool `json:"research_group"` - ResearchConsentStatement string `json:"research_consent_statement"` - ResearchConsentDetails string `json:"research_consent_details"` - ResearchDescription string `json:"research_description"` - ResearchProfile map[string]map[string]any `json:"research_profile"` - Settings *model.GroupSettings `json:"settings"` - Attributes map[string]interface{} `json:"attributes"` - MembersConfig *model.DefaultMembershipConfig `json:"members,omitempty"` - Administrative *bool `json:"administrative"` -} //@name createGroupRequest - -type userGroupShortDetail struct { - ID string `json:"id"` - Title string `json:"title"` - Privacy string `json:"privacy"` - MembershipStatus string `json:"membership_status"` - ResearchOpen bool `json:"research_open"` - ResearchGroup bool `json:"research_group"` -} - -// CreateGroup creates a group -// @Description Creates a group. The user must be part ofĀ urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access. Title must be a unique. Category must be one of the categories list. Privacy can be public or private -// @ID CreateGroup -// @Tags Client -// @Accept json -// @Produce json -// @Param APP header string true "APP" -// @Param data body createGroupRequest true "body data" -// @Success 200 {object} createResponse -// @Security AppUserAuth -// @Router /api/groups [post] -func (h *ApisHandler) CreateGroup(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - - data, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Error on marshal create a group - %s\n", err.Error()) - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - var requestData createGroupRequest - err = json.Unmarshal(data, &requestData) - if err != nil { - log.Printf("Error on unmarshal the create group data - %s\n", err.Error()) - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - //validate - validate := validator.New() - err = validate.Struct(requestData) - if err != nil { - log.Printf("Error on validating create group data - %s\n", err.Error()) - http.Error(w, utils.NewValidationError(err).JSONErrorString(), http.StatusBadRequest) - return - } - - if requestData.ResearchGroup && !current.HasPermission("research_group_admin") { - log.Printf("'%s' is not allowed to create research group '%s'. Only user with research_group_admin permission can create research group", current.Email, requestData.Title) - http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) - return - } - - insertedID, groupErr := h.app.Services.CreateGroup(orgID, current, &model.Group{ - Title: requestData.Title, - Description: requestData.Description, - Category: requestData.Category, - Tags: requestData.Tags, - Privacy: requestData.Privacy, - HiddenForSearch: requestData.Hidden, - ImageURL: requestData.ImageURL, - WebURL: requestData.WebURL, - MembershipQuestions: requestData.MembershipQuestions, - AuthmanGroup: requestData.AuthmanGroup, - AuthmanEnabled: requestData.AuthmanEnabled, - OnlyAdminsCanCreatePolls: requestData.OnlyAdminsCanCreatePolls, - CanJoinAutomatically: requestData.CanJoinAutomatically, - AttendanceGroup: requestData.AttendanceGroup, - ResearchGroup: requestData.ResearchGroup, - ResearchOpen: requestData.ResearchOpen, - ResearchConsentStatement: requestData.ResearchConsentStatement, - ResearchConsentDetails: requestData.ResearchConsentDetails, - ResearchDescription: requestData.ResearchDescription, - ResearchProfile: requestData.ResearchProfile, - Settings: requestData.Settings, - Attributes: requestData.Attributes, - Administrative: requestData.Administrative, - }, requestData.MembersConfig) - if groupErr != nil { - log.Println(groupErr.Error()) - http.Error(w, groupErr.JSONErrorString(), http.StatusBadRequest) - return - } - - if insertedID != nil { - data, err = json.Marshal(createResponse{InsertedID: *insertedID}) - if err != nil { - log.Println("Error on marshal create group response") - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) - w.Write(data) - return - } - w.WriteHeader(http.StatusOK) -} - -type createGroupV3Request struct { - Title string `json:"title" validate:"required"` - Description *string `json:"description"` - Category string `json:"category"` - Tags []string `json:"tags"` - Privacy string `json:"privacy" validate:"required,oneof=public private"` - Hidden bool `json:"hidden_for_search"` - CreatorName string `json:"creator_name"` - CreatorEmail string `json:"creator_email"` - CreatorPhotoURL string `json:"creator_photo_url"` - ImageURL *string `json:"image_url"` - WebURL *string `json:"web_url"` - MembershipQuestions []string `json:"membership_questions"` - AuthmanEnabled bool `json:"authman_enabled"` - AuthmanGroup *string `json:"authman_group"` - OnlyAdminsCanCreatePolls bool `json:"only_admins_can_create_polls" ` - CanJoinAutomatically bool `json:"can_join_automatically"` - AttendanceGroup bool `json:"attendance_group" ` - ResearchOpen bool `json:"research_open"` - ResearchGroup bool `json:"research_group"` - ResearchConsentStatement string `json:"research_consent_statement"` - ResearchConsentDetails string `json:"research_consent_details"` - ResearchDescription string `json:"research_description"` - ResearchProfile map[string]map[string]any `json:"research_profile"` - Settings *model.GroupSettings `json:"settings"` - Attributes map[string]interface{} `json:"attributes"` - MembershipStatuses model.MembershipStatuses `json:"members,omitempty"` - Administrative *bool `json:"administrative"` -} //@name createGroupRequest - -// CreateGroupV3 Creates a group -// @Description Creates a group. Title must be a unique. Category must be one of the categories list. Privacy can be public or private -// @ID CreateGroupV3 -// @Tags Client -// @Accept json -// @Produce json -// @Param APP header string true "APP" -// @Param data body createGroupV3Request true "body data" -// @Success 200 {object} createResponse -// @Security AppUserAuth -// @Router /api/v3/groups [post] -func (h *ApisHandler) CreateGroupV3(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - - data, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Error on marshal create a group - %s\n", err.Error()) - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - var requestData createGroupV3Request - err = json.Unmarshal(data, &requestData) - if err != nil { - log.Printf("Error on unmarshal the create group data - %s\n", err.Error()) - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - //validate - validate := validator.New() - err = validate.Struct(requestData) - if err != nil { - log.Printf("Error on validating create group data - %s\n", err.Error()) - http.Error(w, utils.NewValidationError(err).JSONErrorString(), http.StatusBadRequest) - return - } - - if requestData.ResearchGroup && !current.HasPermission("research_group_admin") { - log.Printf("'%s' is not allowed to create research group '%s'. Only user with research_group_admin permission can create research group", current.Email, requestData.Title) - http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) - return - } - - insertedID, groupErr := h.app.Services.CreateGroupV3(orgID, current, &model.Group{ - Title: requestData.Title, - Description: requestData.Description, - Category: requestData.Category, - Tags: requestData.Tags, - Privacy: requestData.Privacy, - HiddenForSearch: requestData.Hidden, - ImageURL: requestData.ImageURL, - WebURL: requestData.WebURL, - MembershipQuestions: requestData.MembershipQuestions, - AuthmanGroup: requestData.AuthmanGroup, - AuthmanEnabled: requestData.AuthmanEnabled, - OnlyAdminsCanCreatePolls: requestData.OnlyAdminsCanCreatePolls, - CanJoinAutomatically: requestData.CanJoinAutomatically, - AttendanceGroup: requestData.AttendanceGroup, - ResearchGroup: requestData.ResearchGroup, - ResearchOpen: requestData.ResearchOpen, - ResearchConsentStatement: requestData.ResearchConsentStatement, - ResearchConsentDetails: requestData.ResearchConsentDetails, - ResearchDescription: requestData.ResearchDescription, - ResearchProfile: requestData.ResearchProfile, - Settings: requestData.Settings, - Attributes: requestData.Attributes, - Administrative: requestData.Administrative, - }, requestData.MembershipStatuses) - if groupErr != nil { - log.Println(groupErr.Error()) - http.Error(w, groupErr.JSONErrorString(), http.StatusBadRequest) - return - } - - if insertedID != nil { - data, err = json.Marshal(createResponse{InsertedID: *insertedID}) - if err != nil { - log.Println("Error on marshal create group response") - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) - w.Write(data) - return - } - w.WriteHeader(http.StatusOK) -} - -type updateGroupRequest struct { - Title string `json:"title" validate:"required"` - Description *string `json:"description"` - Category string `json:"category"` - Tags []string `json:"tags"` - Privacy string `json:"privacy" validate:"required,oneof=public private"` - Hidden bool `json:"hidden_for_search"` - ImageURL *string `json:"image_url"` - WebURL *string `json:"web_url"` - MembershipQuestions []string `json:"membership_questions"` - AuthmanEnabled bool `json:"authman_enabled"` - AuthmanGroup *string `json:"authman_group"` - OnlyAdminsCanCreatePolls bool `json:"only_admins_can_create_polls"` - CanJoinAutomatically bool `json:"can_join_automatically"` - BlockNewMembershipRequests bool `json:"block_new_membership_requests"` - AttendanceGroup bool `json:"attendance_group" ` - ResearchOpen bool `json:"research_open"` - ResearchGroup bool `json:"research_group"` - ResearchConsentStatement string `json:"research_consent_statement"` - ResearchConsentDetails string `json:"research_consent_details"` - ResearchDescription string `json:"research_description"` - ResearchProfile map[string]map[string]any `json:"research_profile"` - Settings *model.GroupSettings `json:"settings"` - Attributes map[string]interface{} `json:"attributes"` -} //@name updateGroupRequest - -// UpdateGroup updates a group -// @Description Updates a group. -// @ID UpdateGroup -// @Tags Client -// @Accept json -// @Produce json -// @Param APP header string true "APP" -// @Param data body updateGroupRequest true "body data" -// @Param id path string true "ID" -// @Success 200 {string} Successfully updated -// @Security AppUserAuth -// @Router /api/groups/{id} [put] -func (h *ApisHandler) UpdateGroup(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - //validate input - params := mux.Vars(r) - id := params["id"] - if len(id) <= 0 { - log.Println("Group id is required") - http.Error(w, utils.NewMissingParamError("Group id is required").JSONErrorString(), http.StatusBadRequest) - return - } - - data, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Error on marshal the update group item - %s\n", err.Error()) - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - var requestData updateGroupRequest - err = json.Unmarshal(data, &requestData) - if err != nil { - log.Printf("Error on unmarshal the update group request data - %s\n", err.Error()) - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - validate := validator.New() - err = validate.Struct(requestData) - if err != nil { - log.Printf("Error on validating update group data - %s\n", err.Error()) - http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) - return - } - - //check if allowed to update - group, err := h.app.Services.GetGroup(orgID, current, id) - if group.CurrentMember == nil || !group.CurrentMember.IsAdmin() { - log.Printf("%s is not allowed to update group settings '%s'. Only group admin could update a group", current.Email, group.Title) - http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) - return - } - if (requestData.ResearchGroup || group.ResearchGroup) && !current.HasPermission("research_group_admin") { - log.Printf("'%s' is not allowed to update research group '%s'. Only user with research_group_admin permission can update research group", current.Email, group.Title) - http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) - return - } - - groupErr := h.app.Services.UpdateGroup(orgID, current, &model.Group{ - ID: id, - Title: requestData.Title, - Description: requestData.Description, - Category: requestData.Category, - Tags: requestData.Tags, - Privacy: requestData.Privacy, - HiddenForSearch: requestData.Hidden, - ImageURL: requestData.ImageURL, - WebURL: requestData.WebURL, - MembershipQuestions: requestData.MembershipQuestions, - AuthmanGroup: requestData.AuthmanGroup, - AuthmanEnabled: requestData.AuthmanEnabled, - OnlyAdminsCanCreatePolls: requestData.OnlyAdminsCanCreatePolls, - CanJoinAutomatically: requestData.CanJoinAutomatically, - AttendanceGroup: requestData.AttendanceGroup, - - ResearchGroup: requestData.ResearchGroup, - ResearchOpen: requestData.ResearchOpen, - ResearchConsentStatement: requestData.ResearchConsentStatement, - ResearchConsentDetails: requestData.ResearchConsentDetails, - ResearchDescription: requestData.ResearchDescription, - ResearchProfile: requestData.ResearchProfile, - Settings: requestData.Settings, - Attributes: requestData.Attributes, - }) - if groupErr != nil { - log.Printf("Error on updating group - %s\n", err) - http.Error(w, groupErr.JSONErrorString(), http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - w.Write([]byte("Successfully updated")) -} - -// GetGroupStats Retrieves stats for a group by id -// @Description Retrieves stats for a group by id -// @ID GetGroupStats -// @Tags Client -// @Accept json -// @Param APP header string true "APP" -// @Param group-id path string true "Group ID" -// @Success 200 {array} model.GroupStats -// @Security AppUserAuth -// @Router /api/group/{group-id}/stats [get] -func (h *ApisHandler) GetGroupStats(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - //validate input - params := mux.Vars(r) - groupID := params["id"] - if len(groupID) <= 0 { - log.Println("id is required") - http.Error(w, "id is required", http.StatusBadRequest) - return - } - - group, err := h.app.Services.GetGroup(orgID, current, groupID) - if err != nil { - log.Printf("error getting group - %s", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if group == nil { - log.Printf("error getting group stats - %s", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - data, err := json.Marshal(group.Stats) - if err != nil { - log.Println("Error on marshal the group stats") - 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) -} - -// DeleteGroup deletes a group -// @Description Deletes a group. -// @ID DeleteGroup -// @Tags Client -// @Accept json -// @Produce json -// @Param APP header string true "APP" -// @Param id path string true "ID" -// @Success 200 {string} Successfully deleted -// @Security AppUserAuth -// @Router /api/group/{id} [delete] -func (h *ApisHandler) DeleteGroup(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - //validate input - params := mux.Vars(r) - id := params["id"] - if len(id) <= 0 { - log.Println("Group id is required") - http.Error(w, "Group id is required", http.StatusBadRequest) - return - } - - //check if allowed to delete - group, err := h.app.Services.GetGroup(orgID, current, id) - if err != nil { - log.Println(err.Error()) - http.Error(w, utils.NewServerError().JSONErrorString(), http.StatusInternalServerError) - return - } - if group.CurrentMember == nil || !group.CurrentMember.IsAdmin() { - log.Printf("%s is not allowed to update group settings '%s'. Only group admin could delete group", current.Email, group.Title) - http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) - return - } - if group.AuthmanEnabled && !current.HasPermission("managed_group_admin") { - log.Printf("%s is not allowed to update group settings '%s'. Only group admin with managed_group_admin permission could delete a managed group", current.Email, group.Title) - http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) - return - } - - err = h.app.Services.DeleteGroup(orgID, current, id) - if err != nil { - log.Printf("Error on deleting group - %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - w.Write([]byte("Successfully deleted")) -} - -// GetGroups gets groups. It can be filtered by category -// @Description Gives the groups list. It can be filtered by category -// @ID GetGroups -// @Tags Client -// @Accept json -// @Param APP header string true "APP" -// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" -// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" -// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" -// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" -// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" -// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." -// @Param data body model.GroupsFilter true "body data" -// @Success 200 {array} model.Group -// @Security APIKeyAuth -// @Security AppUserAuth -// @Router /api/groups [get] -func (h *ApisHandler) GetGroups(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - var groupsFilter model.GroupsFilter - - catogies, ok := r.URL.Query()["category"] - if ok && len(catogies[0]) > 0 { - groupsFilter.Category = &catogies[0] - } - - privacyParam, ok := r.URL.Query()["privacy"] - if ok && len(privacyParam[0]) > 0 { - groupsFilter.Privacy = &privacyParam[0] - } - - titles, ok := r.URL.Query()["title"] - if ok && len(titles[0]) > 0 { - groupsFilter.Title = &titles[0] - } - - offsets, ok := r.URL.Query()["offset"] - if ok && len(offsets[0]) > 0 { - val, err := strconv.ParseInt(offsets[0], 0, 64) - if err == nil { - groupsFilter.Offset = &val - } - } - - limits, ok := r.URL.Query()["limit"] - if ok && len(limits[0]) > 0 { - val, err := strconv.ParseInt(limits[0], 0, 64) - if err == nil { - groupsFilter.Limit = &val - } - } - - orders, ok := r.URL.Query()["order"] - if ok && len(orders[0]) > 0 { - groupsFilter.Order = &orders[0] - } - - hiddens, ok := r.URL.Query()["include_hidden"] - if ok && len(hiddens[0]) > 0 { - if strings.ToLower(hiddens[0]) == "true" { - val := true - groupsFilter.IncludeHidden = &val - } - } - - if groupsFilter.ResearchGroup == nil { - b := false - groupsFilter.ResearchGroup = &b - } - - _, 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) - return - } - - groupIDs := []string{} - for _, grouop := range groups { - groupIDs = append(groupIDs, grouop.ID) - } - - membershipCollection, err := h.app.Services.FindGroupMemberships(orgID, model.MembershipFilter{ - GroupIDs: groupIDs, - }) - if err != nil { - log.Printf("apis.GetGroups() unable to retrieve memberships: %s", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - for index, group := range groups { - group.ApplyLegacyMembership(membershipCollection) - groups[index] = group - } - - data, err := json.Marshal(groups) - if err != nil { - log.Println("apis.GetGroups() 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) -} - -type getUserGroupsResponse struct { - ID string `json:"id"` - Category string `json:"category"` - Title string `json:"title"` - Privacy string `json:"privacy"` - Description *string `json:"description"` - ImageURL *string `json:"image_url"` - WebURL *string `json:"web_url"` - Tags []string `json:"tags"` - MembershipQuestions []string `json:"membership_questions"` - - Members []struct { - ID string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - PhotoURL string `json:"photo_url"` - Status string `json:"status"` - RejectedReason string `json:"rejected_reason"` - - MemberAnswers []struct { - Question string `json:"question"` - Answer string `json:"answer"` - } `json:"member_answers"` - - DateCreated time.Time `json:"date_created"` - DateUpdated *time.Time `json:"date_updated"` - } `json:"members"` - - DateCreated time.Time `json:"date_created"` - DateUpdated *time.Time `json:"date_updated"` -} // @name getUserGroupsResponse - -// GetUserGroups gets the user groups. -// @Description Gives the user groups. -// @ID GetUserGroups -// @Tags Client -// @Accept json -// @Param APP header string true "APP" -// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" -// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" -// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" -// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" -// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" -// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." -// @Param data body model.GroupsFilter true "body data" -// @Success 200 {array} getUserGroupsResponse -// @Security AppUserAuth -// @Security APIKeyAuth -// @Router /api/user/groups [get] -func (h *ApisHandler) GetUserGroups(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - - var groupsFilter model.GroupsFilter - - catogies, ok := r.URL.Query()["category"] - if ok && len(catogies[0]) > 0 { - groupsFilter.Category = &catogies[0] - } - - privacyParam, ok := r.URL.Query()["privacy"] - if ok && len(privacyParam[0]) > 0 { - groupsFilter.Privacy = &privacyParam[0] - } - - titles, ok := r.URL.Query()["title"] - if ok && len(titles[0]) > 0 { - groupsFilter.Title = &titles[0] - } - - offsets, ok := r.URL.Query()["offset"] - if ok && len(offsets[0]) > 0 { - val, err := strconv.ParseInt(offsets[0], 0, 64) - if err == nil { - groupsFilter.Offset = &val - } - } - - limits, ok := r.URL.Query()["limit"] - if ok && len(limits[0]) > 0 { - val, err := strconv.ParseInt(limits[0], 0, 64) - if err == nil { - groupsFilter.Limit = &val - } - } - - orders, ok := r.URL.Query()["order"] - if ok && len(orders[0]) > 0 { - groupsFilter.Order = &orders[0] - } - - requestData, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("apis.GetUserGroups() 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("apis.GetUserGroups() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) - } - } - - if groupsFilter.ResearchGroup == nil { - b := false - groupsFilter.ResearchGroup = &b - } - - groups, err := h.app.Services.GetUserGroups(orgID, current, groupsFilter) - if err != nil { - log.Printf("apis.GetUserGroups() error getting user groups - %s", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - groupIDs := []string{} - for _, grouop := range groups { - groupIDs = append(groupIDs, grouop.ID) - } - - membershipCollection, err := h.app.Services.FindGroupMemberships(orgID, model.MembershipFilter{ - GroupIDs: groupIDs, - }) - if err != nil { - log.Printf("apis.GetUserGroups() unable to retrieve memberships: %s", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - for index, group := range groups { - group.ApplyLegacyMembership(membershipCollection) - groups[index] = group - } - - data, err := json.Marshal(groups) - if err != nil { - log.Println("apis.GetUserGroups() error on marshal the user 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) -} - -// LoginUser Logs in the user and refactor the user record and linked data if need -// @Description Logs in the user and refactor the user record and linked data if need +// LoginUser Deprecated: Don't use it! Logs in the user and refactor the user record and linked data if need +// @Description Deprecated: Don't use it! Logs in the user and refactor the user record and linked data if need // @ID LoginUser // @Tags Client // @Success 200 @@ -2801,61 +2080,6 @@ func (h *ApisHandler) GetResearchProfileUserCount(orgID string, current *model.U w.Write(data) } -// GetGroupsFilterStats Gets groups filter stats -// @Description Gets groups filter stats -// @ID GetGroupsFilterStats -// @Tags Client -// @Accept json -// @Param APP header string true "APP" -// @Param data body model.StatsFilter true "body data" -// @Success 200 -// @Security AppUserAuth -// @Router /api/group/stats [post] -func (h *ApisHandler) GetGroupsFilterStats(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { - data, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Error on read GetGroupsFilterStats - %s\n", err.Error()) - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - var filter model.StatsFilter - err = json.Unmarshal(data, &filter) - if err != nil { - log.Printf("error on unmarshal GetGroupsFilterStats() - %s", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - stats, err := h.app.Services.GetGroupFilterStats(orgID, current, filter) - if err != nil { - log.Println(err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - var result map[string]int64 - if stats != nil { - result = stats.Stats - } else { - result = make(map[string]int64) - log.Println("no stats found for the provided filter") - http.Error(w, "no stats found for the provided filter", http.StatusNotFound) - return - } - - data, err = json.Marshal(result) - if err != nil { - log.Printf("error on marshal GetGroupsFilterStats - %s", err.Error()) - 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) -} - // reportAbuseGroupRequestBody request body for report abuse group API call type reportAbuseGroupRequestBody struct { Comment string `json:"comment"` diff --git a/driver/web/rest/apis_groups.go b/driver/web/rest/apis_groups.go new file mode 100644 index 00000000..5ef9fcfb --- /dev/null +++ b/driver/web/rest/apis_groups.go @@ -0,0 +1,1118 @@ +// Copyright 2022 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rest + +import ( + "encoding/json" + "groups/core/model" + "groups/utils" + "io" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "gopkg.in/go-playground/validator.v9" +) + +// GetGroups gets groups. It can be filtered by category +// @Description Gives the groups list. It can be filtered by category +// @ID GetGroups +// @Tags Client +// @Accept json +// @Param APP header string true "APP" +// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" +// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" +// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" +// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" +// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" +// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." +// @Param data body model.GroupsFilter true "body data" +// @Success 200 {array} model.Group +// @Security APIKeyAuth +// @Security AppUserAuth +// @Router /api/groups [get] +func (h *ApisHandler) GetGroups(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + var groupsFilter model.GroupsFilter + + catogies, ok := r.URL.Query()["category"] + if ok && len(catogies[0]) > 0 { + groupsFilter.Category = &catogies[0] + } + + privacyParam, ok := r.URL.Query()["privacy"] + if ok && len(privacyParam[0]) > 0 { + groupsFilter.Privacy = &privacyParam[0] + } + + titles, ok := r.URL.Query()["title"] + if ok && len(titles[0]) > 0 { + groupsFilter.Title = &titles[0] + } + + offsets, ok := r.URL.Query()["offset"] + if ok && len(offsets[0]) > 0 { + val, err := strconv.ParseInt(offsets[0], 0, 64) + if err == nil { + groupsFilter.Offset = &val + } + } + + limits, ok := r.URL.Query()["limit"] + if ok && len(limits[0]) > 0 { + val, err := strconv.ParseInt(limits[0], 0, 64) + if err == nil { + groupsFilter.Limit = &val + } + } + + orders, ok := r.URL.Query()["order"] + if ok && len(orders[0]) > 0 { + groupsFilter.Order = &orders[0] + } + + hiddens, ok := r.URL.Query()["include_hidden"] + if ok && len(hiddens[0]) > 0 { + if strings.ToLower(hiddens[0]) == "true" { + val := true + groupsFilter.IncludeHidden = &val + } + } + + if groupsFilter.ResearchGroup == nil { + b := false + groupsFilter.ResearchGroup = &b + } + + _, 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) + return + } + + groupIDs := []string{} + for _, grouop := range groups { + groupIDs = append(groupIDs, grouop.ID) + } + + membershipCollection, err := h.app.Services.FindGroupMemberships(orgID, model.MembershipFilter{ + GroupIDs: groupIDs, + }) + if err != nil { + log.Printf("apis.GetGroups() unable to retrieve memberships: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + for index, group := range groups { + group.ApplyLegacyMembership(membershipCollection) + groups[index] = group + } + + data, err := json.Marshal(groups) + if err != nil { + log.Println("apis.GetGroups() 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) +} + +type getUserGroupsResponse struct { + ID string `json:"id"` + Category string `json:"category"` + Title string `json:"title"` + Privacy string `json:"privacy"` + Description *string `json:"description"` + ImageURL *string `json:"image_url"` + WebURL *string `json:"web_url"` + Tags []string `json:"tags"` + MembershipQuestions []string `json:"membership_questions"` + + Members []struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + PhotoURL string `json:"photo_url"` + Status string `json:"status"` + RejectedReason string `json:"rejected_reason"` + + MemberAnswers []struct { + Question string `json:"question"` + Answer string `json:"answer"` + } `json:"member_answers"` + + DateCreated time.Time `json:"date_created"` + DateUpdated *time.Time `json:"date_updated"` + } `json:"members"` + + DateCreated time.Time `json:"date_created"` + DateUpdated *time.Time `json:"date_updated"` +} // @name getUserGroupsResponse + +// GetUserGroups gets the user groups. +// @Description Gives the user groups. +// @ID GetUserGroups +// @Tags Client +// @Accept json +// @Param APP header string true "APP" +// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" +// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" +// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" +// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" +// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" +// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." +// @Param data body model.GroupsFilter true "body data" +// @Success 200 {array} getUserGroupsResponse +// @Security AppUserAuth +// @Security APIKeyAuth +// @Router /api/user/groups [get] +func (h *ApisHandler) GetUserGroups(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + + var groupsFilter model.GroupsFilter + + catogies, ok := r.URL.Query()["category"] + if ok && len(catogies[0]) > 0 { + groupsFilter.Category = &catogies[0] + } + + privacyParam, ok := r.URL.Query()["privacy"] + if ok && len(privacyParam[0]) > 0 { + groupsFilter.Privacy = &privacyParam[0] + } + + titles, ok := r.URL.Query()["title"] + if ok && len(titles[0]) > 0 { + groupsFilter.Title = &titles[0] + } + + offsets, ok := r.URL.Query()["offset"] + if ok && len(offsets[0]) > 0 { + val, err := strconv.ParseInt(offsets[0], 0, 64) + if err == nil { + groupsFilter.Offset = &val + } + } + + limits, ok := r.URL.Query()["limit"] + if ok && len(limits[0]) > 0 { + val, err := strconv.ParseInt(limits[0], 0, 64) + if err == nil { + groupsFilter.Limit = &val + } + } + + orders, ok := r.URL.Query()["order"] + if ok && len(orders[0]) > 0 { + groupsFilter.Order = &orders[0] + } + + requestData, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("apis.GetUserGroups() 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("apis.GetUserGroups() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) + } + } + + if groupsFilter.ResearchGroup == nil { + b := false + groupsFilter.ResearchGroup = &b + } + + groups, err := h.app.Services.GetUserGroups(orgID, current, groupsFilter) + if err != nil { + log.Printf("apis.GetUserGroups() error getting user groups - %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + groupIDs := []string{} + for _, grouop := range groups { + groupIDs = append(groupIDs, grouop.ID) + } + + membershipCollection, err := h.app.Services.FindGroupMemberships(orgID, model.MembershipFilter{ + GroupIDs: groupIDs, + }) + if err != nil { + log.Printf("apis.GetUserGroups() unable to retrieve memberships: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + for index, group := range groups { + group.ApplyLegacyMembership(membershipCollection) + groups[index] = group + } + + data, err := json.Marshal(groups) + if err != nil { + log.Println("apis.GetUserGroups() error on marshal the user 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) +} + +// GetGroupsV2 gets groups. It can be filtered by category, title and privacy. V2 +// @Description Gives the groups list. It can be filtered by category, title and privacy. V2 +// @ID GetGroupsV2 +// @Tags Client +// @Accept json +// @Param APP header string true "APP" +// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" +// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" +// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" +// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" +// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" +// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." +// @Param data body model.GroupsFilter true "body data" +// @Success 200 {array} model.Group +// @Security AppUserAuth +// @Router /api/v2/groups [get] +// @Router /api/v2/groups [post] + +func (h *ApisHandler) GetGroupsV2(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { + + var groupsFilter model.GroupsFilter + + catogies, ok := r.URL.Query()["category"] + if ok && len(catogies[0]) > 0 { + groupsFilter.Category = &catogies[0] + } + + privacyParam, ok := r.URL.Query()["privacy"] + if ok && len(privacyParam[0]) > 0 { + groupsFilter.Privacy = &privacyParam[0] + } + + titles, ok := r.URL.Query()["title"] + if ok && len(titles[0]) > 0 { + groupsFilter.Title = &titles[0] + } + + offsets, ok := r.URL.Query()["offset"] + if ok && len(offsets[0]) > 0 { + val, err := strconv.ParseInt(offsets[0], 0, 64) + if err == nil { + groupsFilter.Offset = &val + } + } + + limits, ok := r.URL.Query()["limit"] + if ok && len(limits[0]) > 0 { + val, err := strconv.ParseInt(limits[0], 0, 64) + if err == nil { + groupsFilter.Limit = &val + } + } + + orders, ok := r.URL.Query()["order"] + if ok && len(orders[0]) > 0 { + groupsFilter.Order = &orders[0] + } + + hiddens, ok := r.URL.Query()["include_hidden"] + if ok && len(hiddens[0]) > 0 { + if strings.ToLower(hiddens[0]) == "true" { + val := true + groupsFilter.IncludeHidden = &val + } + } + + requestData, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("apis.GetGroupsV2() 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("apis.GetGroupsV2() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) + } + } + + if groupsFilter.ResearchGroup == nil { + b := false + groupsFilter.ResearchGroup = &b + } + + _, 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) + return + } + + if groups == nil { + groups = []model.Group{} + } + + data, err := json.Marshal(groups) + if err != nil { + log.Println("apis.GetGroupsV2() 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) +} + +// 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 Client +// @Accept json +// @Param data body model.GroupsFilter true "body data" +// @Success 200 {object} getGroupsResponseV3 +// @Security AppUserAuth +// @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 + + 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) +} + +// GetGroupsFilterStatsV3 Gets groups filter stats +// @Description Gets groups filter stats +// @ID GetGroupsFilterStatsV3 +// @Tags Client +// @Accept json +// @Param APP header string true "APP" +// @Param data body model.StatsFilter true "body data" +// @Success 200 +// @Security AppUserAuth +// @Router /api/v3/groups/stats [post] +func (h *ApisHandler) GetGroupsFilterStatsV3(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("Error on read GetGroupsFilterStats - %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + var filter model.StatsFilter + err = json.Unmarshal(data, &filter) + if err != nil { + log.Printf("error on unmarshal GetGroupsFilterStats() - %s", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + stats, err := h.app.Services.GetGroupFilterStats(orgID, current, filter) + if err != nil { + log.Println(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var result map[string]int64 + if stats != nil { + result = stats.Stats + } else { + result = make(map[string]int64) + log.Println("no stats found for the provided filter") + http.Error(w, "no stats found for the provided filter", http.StatusNotFound) + return + } + + data, err = json.Marshal(result) + if err != nil { + log.Printf("error on marshal GetGroupsFilterStats - %s", err.Error()) + 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 +// @Tags Client +// @Accept json +// @Param APP header string true "APP" +// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" +// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" +// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" +// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" +// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" +// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." +// @Param data body model.GroupsFilter true "body data" +// @Success 200 {array} model.Group +// @Security AppUserAuth +// @Security APIKeyAuth +// @Router /api/v2/user/groups [get] +// @Router /api/v2/user/groups [post] +func (h *ApisHandler) GetUserGroupsV2(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { + var groupsFilter model.GroupsFilter + + catogies, ok := r.URL.Query()["category"] + if ok && len(catogies[0]) > 0 { + groupsFilter.Category = &catogies[0] + } + + privacyParam, ok := r.URL.Query()["privacy"] + if ok && len(privacyParam[0]) > 0 { + groupsFilter.Privacy = &privacyParam[0] + } + + titles, ok := r.URL.Query()["title"] + if ok && len(titles[0]) > 0 { + groupsFilter.Title = &titles[0] + } + + offsets, ok := r.URL.Query()["offset"] + if ok && len(offsets[0]) > 0 { + val, err := strconv.ParseInt(offsets[0], 0, 64) + if err == nil { + groupsFilter.Offset = &val + } + } + + limits, ok := r.URL.Query()["limit"] + if ok && len(limits[0]) > 0 { + val, err := strconv.ParseInt(limits[0], 0, 64) + if err == nil { + groupsFilter.Limit = &val + } + } + + orders, ok := r.URL.Query()["order"] + if ok && len(orders[0]) > 0 { + groupsFilter.Order = &orders[0] + } + + requestData, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("apis.GetUserGroupsV2() 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("apis.GetUserGroupsV2() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) + } + } + + if groupsFilter.ResearchGroup == nil { + b := false + groupsFilter.ResearchGroup = &b + } + + groups, err := h.app.Services.GetUserGroups(clientID, current, groupsFilter) + if err != nil { + log.Printf("apis.GetUserGroupsV2() error getting user groups - %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if groups == nil { + groups = []model.Group{} + } + + data, err := json.Marshal(groups) + if err != nil { + log.Println("apis.GetUserGroupsV2() error on marshal the user 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) +} + +// GetGroupV2 gets a group. V2 +// @Description Gives a group. V2 +// @ID GetGroupV2 +// @Tags Client +// @Accept json +// @Param APP header string true "APP" +// @Param id path string true "ID" +// @Success 200 {object} model.Group +// @Security AppUserAuth +// @Router /api/v2/groups/{id} [get] +func (h *ApisHandler) GetGroupV2(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + if len(id) <= 0 { + log.Println("apis.GetGroupV2() id is required") + http.Error(w, "id is required", http.StatusBadRequest) + return + } + + group, err := h.app.Services.GetGroup(clientID, current, id) + if err != nil { + log.Printf("apis.GetGroupV2() error on getting group %s - %s", id, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if group == nil { + log.Printf("apis.GetGroupV2() group %s not found", id) + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + data, err := json.Marshal(group) + if err != nil { + log.Println("apis.GetGroupV2() error on marshal the group") + 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) +} + +type createGroupRequest struct { + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + Category string `json:"category"` + Tags []string `json:"tags"` + Privacy string `json:"privacy" validate:"required,oneof=public private"` + Hidden bool `json:"hidden_for_search"` + CreatorName string `json:"creator_name"` + CreatorEmail string `json:"creator_email"` + CreatorPhotoURL string `json:"creator_photo_url"` + ImageURL *string `json:"image_url"` + WebURL *string `json:"web_url"` + MembershipQuestions []string `json:"membership_questions"` + AuthmanEnabled bool `json:"authman_enabled"` + AuthmanGroup *string `json:"authman_group"` + OnlyAdminsCanCreatePolls bool `json:"only_admins_can_create_polls" ` + CanJoinAutomatically bool `json:"can_join_automatically"` + AttendanceGroup bool `json:"attendance_group" ` + ResearchOpen bool `json:"research_open"` + ResearchGroup bool `json:"research_group"` + ResearchConsentStatement string `json:"research_consent_statement"` + ResearchConsentDetails string `json:"research_consent_details"` + ResearchDescription string `json:"research_description"` + ResearchProfile map[string]map[string]any `json:"research_profile"` + Settings *model.GroupSettings `json:"settings"` + Attributes map[string]interface{} `json:"attributes"` + MembersConfig *model.DefaultMembershipConfig `json:"members,omitempty"` + Administrative *bool `json:"administrative"` +} //@name createGroupRequest + +type userGroupShortDetail struct { + ID string `json:"id"` + Title string `json:"title"` + Privacy string `json:"privacy"` + MembershipStatus string `json:"membership_status"` + ResearchOpen bool `json:"research_open"` + ResearchGroup bool `json:"research_group"` +} + +// CreateGroup creates a group +// @Description Creates a group. The user must be part ofĀ urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access. Title must be a unique. Category must be one of the categories list. Privacy can be public or private +// @ID CreateGroup +// @Tags Client +// @Accept json +// @Produce json +// @Param APP header string true "APP" +// @Param data body createGroupRequest true "body data" +// @Success 200 {object} createResponse +// @Security AppUserAuth +// @Router /api/groups [post] +func (h *ApisHandler) CreateGroup(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + + data, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("Error on marshal create a group - %s\n", err.Error()) + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + var requestData createGroupRequest + err = json.Unmarshal(data, &requestData) + if err != nil { + log.Printf("Error on unmarshal the create group data - %s\n", err.Error()) + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + //validate + validate := validator.New() + err = validate.Struct(requestData) + if err != nil { + log.Printf("Error on validating create group data - %s\n", err.Error()) + http.Error(w, utils.NewValidationError(err).JSONErrorString(), http.StatusBadRequest) + return + } + + if requestData.ResearchGroup && !current.HasPermission("research_group_admin") { + log.Printf("'%s' is not allowed to create research group '%s'. Only user with research_group_admin permission can create research group", current.Email, requestData.Title) + http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) + return + } + + insertedID, groupErr := h.app.Services.CreateGroup(orgID, current, &model.Group{ + Title: requestData.Title, + Description: requestData.Description, + Category: requestData.Category, + Tags: requestData.Tags, + Privacy: requestData.Privacy, + HiddenForSearch: requestData.Hidden, + ImageURL: requestData.ImageURL, + WebURL: requestData.WebURL, + MembershipQuestions: requestData.MembershipQuestions, + AuthmanGroup: requestData.AuthmanGroup, + AuthmanEnabled: requestData.AuthmanEnabled, + OnlyAdminsCanCreatePolls: requestData.OnlyAdminsCanCreatePolls, + CanJoinAutomatically: requestData.CanJoinAutomatically, + AttendanceGroup: requestData.AttendanceGroup, + ResearchGroup: requestData.ResearchGroup, + ResearchOpen: requestData.ResearchOpen, + ResearchConsentStatement: requestData.ResearchConsentStatement, + ResearchConsentDetails: requestData.ResearchConsentDetails, + ResearchDescription: requestData.ResearchDescription, + ResearchProfile: requestData.ResearchProfile, + Settings: requestData.Settings, + Attributes: requestData.Attributes, + Administrative: requestData.Administrative, + }, requestData.MembersConfig) + if groupErr != nil { + log.Println(groupErr.Error()) + http.Error(w, groupErr.JSONErrorString(), http.StatusBadRequest) + return + } + + if insertedID != nil { + data, err = json.Marshal(createResponse{InsertedID: *insertedID}) + if err != nil { + log.Println("Error on marshal create group response") + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(data) + return + } + w.WriteHeader(http.StatusOK) +} + +type createGroupV3Request struct { + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + Category string `json:"category"` + Tags []string `json:"tags"` + Privacy string `json:"privacy" validate:"required,oneof=public private"` + Hidden bool `json:"hidden_for_search"` + CreatorName string `json:"creator_name"` + CreatorEmail string `json:"creator_email"` + CreatorPhotoURL string `json:"creator_photo_url"` + ImageURL *string `json:"image_url"` + WebURL *string `json:"web_url"` + MembershipQuestions []string `json:"membership_questions"` + AuthmanEnabled bool `json:"authman_enabled"` + AuthmanGroup *string `json:"authman_group"` + OnlyAdminsCanCreatePolls bool `json:"only_admins_can_create_polls" ` + CanJoinAutomatically bool `json:"can_join_automatically"` + AttendanceGroup bool `json:"attendance_group" ` + ResearchOpen bool `json:"research_open"` + ResearchGroup bool `json:"research_group"` + ResearchConsentStatement string `json:"research_consent_statement"` + ResearchConsentDetails string `json:"research_consent_details"` + ResearchDescription string `json:"research_description"` + ResearchProfile map[string]map[string]any `json:"research_profile"` + Settings *model.GroupSettings `json:"settings"` + Attributes map[string]interface{} `json:"attributes"` + MembershipStatuses model.MembershipStatuses `json:"members,omitempty"` + Administrative *bool `json:"administrative"` +} //@name createGroupRequest + +// CreateGroupV3 Creates a group +// @Description Creates a group. Title must be a unique. Category must be one of the categories list. Privacy can be public or private +// @ID CreateGroupV3 +// @Tags Client +// @Accept json +// @Produce json +// @Param APP header string true "APP" +// @Param data body createGroupV3Request true "body data" +// @Success 200 {object} createResponse +// @Security AppUserAuth +// @Router /api/v3/groups [post] +func (h *ApisHandler) CreateGroupV3(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + + data, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("Error on marshal create a group - %s\n", err.Error()) + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + var requestData createGroupV3Request + err = json.Unmarshal(data, &requestData) + if err != nil { + log.Printf("Error on unmarshal the create group data - %s\n", err.Error()) + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + //validate + validate := validator.New() + err = validate.Struct(requestData) + if err != nil { + log.Printf("Error on validating create group data - %s\n", err.Error()) + http.Error(w, utils.NewValidationError(err).JSONErrorString(), http.StatusBadRequest) + return + } + + if requestData.ResearchGroup && !current.HasPermission("research_group_admin") { + log.Printf("'%s' is not allowed to create research group '%s'. Only user with research_group_admin permission can create research group", current.Email, requestData.Title) + http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) + return + } + + insertedID, groupErr := h.app.Services.CreateGroupV3(orgID, current, &model.Group{ + Title: requestData.Title, + Description: requestData.Description, + Category: requestData.Category, + Tags: requestData.Tags, + Privacy: requestData.Privacy, + HiddenForSearch: requestData.Hidden, + ImageURL: requestData.ImageURL, + WebURL: requestData.WebURL, + MembershipQuestions: requestData.MembershipQuestions, + AuthmanGroup: requestData.AuthmanGroup, + AuthmanEnabled: requestData.AuthmanEnabled, + OnlyAdminsCanCreatePolls: requestData.OnlyAdminsCanCreatePolls, + CanJoinAutomatically: requestData.CanJoinAutomatically, + AttendanceGroup: requestData.AttendanceGroup, + ResearchGroup: requestData.ResearchGroup, + ResearchOpen: requestData.ResearchOpen, + ResearchConsentStatement: requestData.ResearchConsentStatement, + ResearchConsentDetails: requestData.ResearchConsentDetails, + ResearchDescription: requestData.ResearchDescription, + ResearchProfile: requestData.ResearchProfile, + Settings: requestData.Settings, + Attributes: requestData.Attributes, + Administrative: requestData.Administrative, + }, requestData.MembershipStatuses) + if groupErr != nil { + log.Println(groupErr.Error()) + http.Error(w, groupErr.JSONErrorString(), http.StatusBadRequest) + return + } + + if insertedID != nil { + data, err = json.Marshal(createResponse{InsertedID: *insertedID}) + if err != nil { + log.Println("Error on marshal create group response") + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(data) + return + } + w.WriteHeader(http.StatusOK) +} + +type updateGroupRequest struct { + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + Category string `json:"category"` + Tags []string `json:"tags"` + Privacy string `json:"privacy" validate:"required,oneof=public private"` + Hidden bool `json:"hidden_for_search"` + ImageURL *string `json:"image_url"` + WebURL *string `json:"web_url"` + MembershipQuestions []string `json:"membership_questions"` + AuthmanEnabled bool `json:"authman_enabled"` + AuthmanGroup *string `json:"authman_group"` + OnlyAdminsCanCreatePolls bool `json:"only_admins_can_create_polls"` + CanJoinAutomatically bool `json:"can_join_automatically"` + BlockNewMembershipRequests bool `json:"block_new_membership_requests"` + AttendanceGroup bool `json:"attendance_group" ` + ResearchOpen bool `json:"research_open"` + ResearchGroup bool `json:"research_group"` + ResearchConsentStatement string `json:"research_consent_statement"` + ResearchConsentDetails string `json:"research_consent_details"` + ResearchDescription string `json:"research_description"` + ResearchProfile map[string]map[string]any `json:"research_profile"` + Settings *model.GroupSettings `json:"settings"` + Attributes map[string]interface{} `json:"attributes"` +} //@name updateGroupRequest + +// UpdateGroup updates a group +// @Description Updates a group. +// @ID UpdateGroup +// @Tags Client +// @Accept json +// @Produce json +// @Param APP header string true "APP" +// @Param data body updateGroupRequest true "body data" +// @Param id path string true "ID" +// @Success 200 {string} Successfully updated +// @Security AppUserAuth +// @Router /api/groups/{id} [put] +func (h *ApisHandler) UpdateGroup(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + //validate input + params := mux.Vars(r) + id := params["id"] + if len(id) <= 0 { + log.Println("Group id is required") + http.Error(w, utils.NewMissingParamError("Group id is required").JSONErrorString(), http.StatusBadRequest) + return + } + + data, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("Error on marshal the update group item - %s\n", err.Error()) + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + var requestData updateGroupRequest + err = json.Unmarshal(data, &requestData) + if err != nil { + log.Printf("Error on unmarshal the update group request data - %s\n", err.Error()) + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + validate := validator.New() + err = validate.Struct(requestData) + if err != nil { + log.Printf("Error on validating update group data - %s\n", err.Error()) + http.Error(w, utils.NewBadJSONError().JSONErrorString(), http.StatusBadRequest) + return + } + + //check if allowed to update + group, err := h.app.Services.GetGroup(orgID, current, id) + if group.CurrentMember == nil || !group.CurrentMember.IsAdmin() { + log.Printf("%s is not allowed to update group settings '%s'. Only group admin could update a group", current.Email, group.Title) + http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) + return + } + if (requestData.ResearchGroup || group.ResearchGroup) && !current.HasPermission("research_group_admin") { + log.Printf("'%s' is not allowed to update research group '%s'. Only user with research_group_admin permission can update research group", current.Email, group.Title) + http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) + return + } + + groupErr := h.app.Services.UpdateGroup(orgID, current, &model.Group{ + ID: id, + Title: requestData.Title, + Description: requestData.Description, + Category: requestData.Category, + Tags: requestData.Tags, + Privacy: requestData.Privacy, + HiddenForSearch: requestData.Hidden, + ImageURL: requestData.ImageURL, + WebURL: requestData.WebURL, + MembershipQuestions: requestData.MembershipQuestions, + AuthmanGroup: requestData.AuthmanGroup, + AuthmanEnabled: requestData.AuthmanEnabled, + OnlyAdminsCanCreatePolls: requestData.OnlyAdminsCanCreatePolls, + CanJoinAutomatically: requestData.CanJoinAutomatically, + AttendanceGroup: requestData.AttendanceGroup, + + ResearchGroup: requestData.ResearchGroup, + ResearchOpen: requestData.ResearchOpen, + ResearchConsentStatement: requestData.ResearchConsentStatement, + ResearchConsentDetails: requestData.ResearchConsentDetails, + ResearchDescription: requestData.ResearchDescription, + ResearchProfile: requestData.ResearchProfile, + Settings: requestData.Settings, + Attributes: requestData.Attributes, + }) + if groupErr != nil { + log.Printf("Error on updating group - %s\n", err) + http.Error(w, groupErr.JSONErrorString(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Successfully updated")) +} + +// GetGroupStats Retrieves stats for a group by id +// @Description Retrieves stats for a group by id +// @ID GetGroupStats +// @Tags Client +// @Accept json +// @Param APP header string true "APP" +// @Param group-id path string true "Group ID" +// @Success 200 {array} model.GroupStats +// @Security AppUserAuth +// @Router /api/group/{group-id}/stats [get] +func (h *ApisHandler) GetGroupStats(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + //validate input + params := mux.Vars(r) + groupID := params["id"] + if len(groupID) <= 0 { + log.Println("id is required") + http.Error(w, "id is required", http.StatusBadRequest) + return + } + + group, err := h.app.Services.GetGroup(orgID, current, groupID) + if err != nil { + log.Printf("error getting group - %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if group == nil { + log.Printf("error getting group stats - %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data, err := json.Marshal(group.Stats) + if err != nil { + log.Println("Error on marshal the group stats") + 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) +} + +// DeleteGroup deletes a group +// @Description Deletes a group. +// @ID DeleteGroup +// @Tags Client +// @Accept json +// @Produce json +// @Param APP header string true "APP" +// @Param id path string true "ID" +// @Success 200 {string} Successfully deleted +// @Security AppUserAuth +// @Router /api/group/{id} [delete] +func (h *ApisHandler) DeleteGroup(orgID string, current *model.User, w http.ResponseWriter, r *http.Request) { + //validate input + params := mux.Vars(r) + id := params["id"] + if len(id) <= 0 { + log.Println("Group id is required") + http.Error(w, "Group id is required", http.StatusBadRequest) + return + } + + //check if allowed to delete + group, err := h.app.Services.GetGroup(orgID, current, id) + if err != nil { + log.Println(err.Error()) + http.Error(w, utils.NewServerError().JSONErrorString(), http.StatusInternalServerError) + return + } + if group.CurrentMember == nil || !group.CurrentMember.IsAdmin() { + log.Printf("%s is not allowed to update group settings '%s'. Only group admin could delete group", current.Email, group.Title) + http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) + return + } + if group.AuthmanEnabled && !current.HasPermission("managed_group_admin") { + log.Printf("%s is not allowed to update group settings '%s'. Only group admin with managed_group_admin permission could delete a managed group", current.Email, group.Title) + http.Error(w, utils.NewForbiddenError().JSONErrorString(), http.StatusForbidden) + return + } + + err = h.app.Services.DeleteGroup(orgID, current, id) + if err != nil { + log.Printf("Error on deleting group - %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Successfully deleted")) +} diff --git a/driver/web/rest/apis_v2.go b/driver/web/rest/apis_v2.go deleted file mode 100644 index 17af63d0..00000000 --- a/driver/web/rest/apis_v2.go +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright 2022 Board of Trustees of the University of Illinois. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package rest - -import ( - "encoding/json" - "groups/core/model" - "io" - "log" - "net/http" - "strconv" - "strings" - - "github.com/gorilla/mux" -) - -// GetGroupsV2 gets groups. It can be filtered by category, title and privacy. V2 -// @Description Gives the groups list. It can be filtered by category, title and privacy. V2 -// @ID GetGroupsV2 -// @Tags Client -// @Accept json -// @Param APP header string true "APP" -// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" -// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" -// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" -// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" -// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" -// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." -// @Param data body model.GroupsFilter true "body data" -// @Success 200 {array} model.Group -// @Security AppUserAuth -// @Router /api/v2/groups [get] -// @Router /api/v2/groups [post] -func (h *ApisHandler) GetGroupsV2(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { - - var groupsFilter model.GroupsFilter - - catogies, ok := r.URL.Query()["category"] - if ok && len(catogies[0]) > 0 { - groupsFilter.Category = &catogies[0] - } - - privacyParam, ok := r.URL.Query()["privacy"] - if ok && len(privacyParam[0]) > 0 { - groupsFilter.Privacy = &privacyParam[0] - } - - titles, ok := r.URL.Query()["title"] - if ok && len(titles[0]) > 0 { - groupsFilter.Title = &titles[0] - } - - offsets, ok := r.URL.Query()["offset"] - if ok && len(offsets[0]) > 0 { - val, err := strconv.ParseInt(offsets[0], 0, 64) - if err == nil { - groupsFilter.Offset = &val - } - } - - limits, ok := r.URL.Query()["limit"] - if ok && len(limits[0]) > 0 { - val, err := strconv.ParseInt(limits[0], 0, 64) - if err == nil { - groupsFilter.Limit = &val - } - } - - orders, ok := r.URL.Query()["order"] - if ok && len(orders[0]) > 0 { - groupsFilter.Order = &orders[0] - } - - hiddens, ok := r.URL.Query()["include_hidden"] - if ok && len(hiddens[0]) > 0 { - if strings.ToLower(hiddens[0]) == "true" { - val := true - groupsFilter.IncludeHidden = &val - } - } - - requestData, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("apis.GetGroupsV2() 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("apis.GetGroupsV2() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) - } - } - - if groupsFilter.ResearchGroup == nil { - b := false - groupsFilter.ResearchGroup = &b - } - - _, 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) - return - } - - if groups == nil { - groups = []model.Group{} - } - - data, err := json.Marshal(groups) - if err != nil { - log.Println("apis.GetGroupsV2() 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) -} - -// 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 Client -// @Accept json -// @Param data body model.GroupsFilter true "body data" -// @Success 200 {object} getGroupsResponseV3 -// @Security AppUserAuth -// @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 - - 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 -// @Tags Client -// @Accept json -// @Param APP header string true "APP" -// @Param title query string false "Deprecated - instead use request body filter! Filtering by group's title (case-insensitive)" -// @Param category query string false "Deprecated - instead use request body filter! category - filter by category" -// @Param privacy query string false "Deprecated - instead use request body filter! privacy - filter by privacy" -// @Param offset query string false "Deprecated - instead use request body filter! offset - skip number of records" -// @Param limit query string false "Deprecated - instead use request body filter! limit - limit the result" -// @Param include_hidden query string false "Deprecated - instead use request body filter! include_hidden - Includes hidden groups if a search by title is performed. Possible value is true. Default false." -// @Param data body model.GroupsFilter true "body data" -// @Success 200 {array} model.Group -// @Security AppUserAuth -// @Security APIKeyAuth -// @Router /api/v2/user/groups [get] -// @Router /api/v2/user/groups [post] -func (h *ApisHandler) GetUserGroupsV2(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { - var groupsFilter model.GroupsFilter - - catogies, ok := r.URL.Query()["category"] - if ok && len(catogies[0]) > 0 { - groupsFilter.Category = &catogies[0] - } - - privacyParam, ok := r.URL.Query()["privacy"] - if ok && len(privacyParam[0]) > 0 { - groupsFilter.Privacy = &privacyParam[0] - } - - titles, ok := r.URL.Query()["title"] - if ok && len(titles[0]) > 0 { - groupsFilter.Title = &titles[0] - } - - offsets, ok := r.URL.Query()["offset"] - if ok && len(offsets[0]) > 0 { - val, err := strconv.ParseInt(offsets[0], 0, 64) - if err == nil { - groupsFilter.Offset = &val - } - } - - limits, ok := r.URL.Query()["limit"] - if ok && len(limits[0]) > 0 { - val, err := strconv.ParseInt(limits[0], 0, 64) - if err == nil { - groupsFilter.Limit = &val - } - } - - orders, ok := r.URL.Query()["order"] - if ok && len(orders[0]) > 0 { - groupsFilter.Order = &orders[0] - } - - requestData, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("apis.GetUserGroupsV2() 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("apis.GetUserGroupsV2() error on unmarshal model.GroupsFilter request body - %s\n", err.Error()) - } - } - - if groupsFilter.ResearchGroup == nil { - b := false - groupsFilter.ResearchGroup = &b - } - - groups, err := h.app.Services.GetUserGroups(clientID, current, groupsFilter) - if err != nil { - log.Printf("apis.GetUserGroupsV2() error getting user groups - %s", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if groups == nil { - groups = []model.Group{} - } - - data, err := json.Marshal(groups) - if err != nil { - log.Println("apis.GetUserGroupsV2() error on marshal the user 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) -} - -// GetGroupV2 gets a group. V2 -// @Description Gives a group. V2 -// @ID GetGroupV2 -// @Tags Client -// @Accept json -// @Param APP header string true "APP" -// @Param id path string true "ID" -// @Success 200 {object} model.Group -// @Security AppUserAuth -// @Router /api/v2/groups/{id} [get] -func (h *ApisHandler) GetGroupV2(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - id := params["id"] - if len(id) <= 0 { - log.Println("apis.GetGroupV2() id is required") - http.Error(w, "id is required", http.StatusBadRequest) - return - } - - group, err := h.app.Services.GetGroup(clientID, current, id) - if err != nil { - log.Printf("apis.GetGroupV2() error on getting group %s - %s", id, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - if group == nil { - log.Printf("apis.GetGroupV2() group %s not found", id) - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - - data, err := json.Marshal(group) - if err != nil { - log.Println("apis.GetGroupV2() error on marshal the group") - 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) -} From 95318accfca0710e0067cff15a02fe53f2a19d84 Mon Sep 17 00:00:00 2001 From: Mladen Date: Tue, 16 Dec 2025 13:38:56 +0200 Subject: [PATCH 14/22] Changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eacfe7b4..a2e99af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [1.73.3] - 2025-12-16 +### Fixed +- USABILITY UI CleanUp: Group Filters - API improvements. Update version of Golang and the external libraties. Additional fix for api doc [#613](https://github.com/rokwire/groups-building-block/issues/613) + ## [1.73.2] - 2025-12-15 ### Fixed -- SABILITY UI CleanUp: Group Filters - API improvements. Fix api doc [#613](https://github.com/rokwire/groups-building-block/issues/613) +- USABILITY UI CleanUp: Group Filters - API improvements. Fix api doc [#613](https://github.com/rokwire/groups-building-block/issues/613) ## [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) +- USABILITY 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 From 464b986864f196b90601b7d8fa267c00714f2ac7 Mon Sep 17 00:00:00 2001 From: Mladen Date: Tue, 16 Dec 2025 22:30:01 +0200 Subject: [PATCH 15/22] Fix member_status field --- core/model/filters.go | 2 +- driven/storage/adapter.go | 4 ++-- driver/web/rest/apis_groups.go | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/model/filters.go b/core/model/filters.go index bf2a97ba..64446bc0 100644 --- a/core/model/filters.go +++ b/core/model/filters.go @@ -21,7 +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 + MemberStatus []string `json:"member_status"` // member user status Title *string `json:"title"` // group title Category *string `json:"category"` // group category Privacy *string `json:"privacy"` // group privacy diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 9204d936..83a79a41 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -772,7 +772,7 @@ func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, cl ID: groupsFilter.MemberID, UserID: userID, ExternalID: groupsFilter.MemberExternalID, - Statuses: groupsFilter.MemberStatuses, + Statuses: groupsFilter.MemberStatus, }) if err != nil { return nil, model.MembershipCollection{}, err @@ -787,7 +787,7 @@ func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, cl } filter := bson.D{} - if len(groupsFilter.MemberStatuses) > 0 { + if len(groupsFilter.MemberStatus) > 0 { filter = append(filter, bson.E{Key: "_id", Value: bson.M{"$in": groupIDs}}) } if groupsFilter.GroupIDs != nil { diff --git a/driver/web/rest/apis_groups.go b/driver/web/rest/apis_groups.go index 5ef9fcfb..55713cce 100644 --- a/driver/web/rest/apis_groups.go +++ b/driver/web/rest/apis_groups.go @@ -300,7 +300,6 @@ func (h *ApisHandler) GetUserGroups(orgID string, current *model.User, w http.Re // @Security AppUserAuth // @Router /api/v2/groups [get] // @Router /api/v2/groups [post] - func (h *ApisHandler) GetGroupsV2(clientID string, current *model.User, w http.ResponseWriter, r *http.Request) { var groupsFilter model.GroupsFilter From 106dbb57465d6828922f641bcd70153ca8b72245 Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 17 Dec 2025 12:40:02 +0200 Subject: [PATCH 16/22] Fix deviation between groups filtering and stats logic --- core/interfaces.go | 4 ++-- core/interfaces_client.go | 4 ++-- core/services.go | 4 ++-- driven/storage/adapter.go | 8 +++++++- driven/storage/adapter_v3.go | 6 +++--- driver/web/rest/apis_groups.go | 4 ++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/core/interfaces.go b/core/interfaces.go index cd22c2ec..e4d8e9ba 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -41,7 +41,7 @@ type Services interface { GetAllGroupsUnsecured() ([]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) + GetGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter, skipMembershipCheck bool) (*model.StatsResult, error) GetUserGroups(clientID string, current *model.User, filter model.GroupsFilter) ([]model.Group, error) DeleteUser(clientID string, current *model.User) error ReportGroupAsAbuse(clientID string, current *model.User, group *model.Group, comment string) error @@ -193,7 +193,7 @@ type Storage interface { DeleteManagedGroupConfig(id string, clientID string) error // V3 - CalculateGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter) (*model.StatsResult, error) + CalculateGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter, skipMembershipCheck bool) (*model.StatsResult, error) FindGroupsV3(context storage.TransactionContext, clientID string, filter model.GroupsFilter) ([]model.Group, error) FindGroupMemberships(clientID string, filter model.MembershipFilter) (model.MembershipCollection, error) FindGroupMembershipsWithContext(context storage.TransactionContext, clientID string, filter model.MembershipFilter) (model.MembershipCollection, error) diff --git a/core/interfaces_client.go b/core/interfaces_client.go index 81947920..26cff425 100644 --- a/core/interfaces_client.go +++ b/core/interfaces_client.go @@ -89,8 +89,8 @@ func (s *servicesImpl) GetGroup(clientID string, current *model.User, id string) return s.app.getGroup(clientID, current, id) } -func (s *servicesImpl) GetGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter) (*model.StatsResult, error) { - return s.app.getGroupFilterStats(clientID, current, filter) +func (s *servicesImpl) GetGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter, skipMembershipCheck bool) (*model.StatsResult, error) { + return s.app.getGroupFilterStats(clientID, current, filter, skipMembershipCheck) } func (s *servicesImpl) GetGroupStats(clientID string, id string) (*model.GroupStats, error) { diff --git a/core/services.go b/core/services.go index 07a93305..a0b06de3 100644 --- a/core/services.go +++ b/core/services.go @@ -460,8 +460,8 @@ func (app *Application) getGroup(clientID string, current *model.User, id string return group, nil } -func (app *Application) getGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter) (*model.StatsResult, error) { - return app.storage.CalculateGroupFilterStats(clientID, current, filter) +func (app *Application) getGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter, skipMembershipCheck bool) (*model.StatsResult, error) { + return app.storage.CalculateGroupFilterStats(clientID, current, filter, skipMembershipCheck) } func (app *Application) applyMembershipApproval(clientID string, current *model.User, membershipID string, approve bool, rejectReason string) error { diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 83a79a41..c03485f8 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -764,13 +764,19 @@ func (sa *Adapter) buildMainQuery(context TransactionContext, userID *string, cl var memberships model.MembershipCollection + var userIDFilter *string + if userID != nil && !skipMembershipCheck { + userIDFilter = userID + } else if groupsFilter.MemberUserID != nil { + userIDFilter = groupsFilter.MemberUserID + } // Credits to Ryan Oberlander suggest if userID != nil || groupsFilter.MemberID != nil || groupsFilter.MemberExternalID != nil { // find group memberships var err error memberships, err = sa.FindGroupMembershipsWithContext(context, clientID, model.MembershipFilter{ ID: groupsFilter.MemberID, - UserID: userID, + UserID: userIDFilter, ExternalID: groupsFilter.MemberExternalID, Statuses: groupsFilter.MemberStatus, }) diff --git a/driven/storage/adapter_v3.go b/driven/storage/adapter_v3.go index 5bf04ae2..efbd0be3 100644 --- a/driven/storage/adapter_v3.go +++ b/driven/storage/adapter_v3.go @@ -168,10 +168,10 @@ func (sa *Adapter) buildGroupsFilter11(clientID string, context TransactionConte } // CalculateGroupFilterStats Generates the stats for a given filter -func (sa *Adapter) CalculateGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter) (*model.StatsResult, error) { +func (sa *Adapter) CalculateGroupFilterStats(clientID string, current *model.User, filter model.StatsFilter, skipMembershipCheck bool) (*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, skipMembershipCheck) if err != nil { return err } @@ -181,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, skipMembershipCheck) if err != nil { return err } diff --git a/driver/web/rest/apis_groups.go b/driver/web/rest/apis_groups.go index 55713cce..e43df36e 100644 --- a/driver/web/rest/apis_groups.go +++ b/driver/web/rest/apis_groups.go @@ -423,7 +423,7 @@ func (h *ApisHandler) GetGroupsV3(clientID string, current *model.User, w http.R groupsFilter.ResearchGroup = &b } - count, groups, err := h.app.Admin.GetGroups(clientID, current, groupsFilter) + count, groups, err := h.app.Services.GetGroups(clientID, current, groupsFilter) if err != nil { log.Printf("adminapis.GetGroupsV3() error getting groups - %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -477,7 +477,7 @@ func (h *ApisHandler) GetGroupsFilterStatsV3(orgID string, current *model.User, return } - stats, err := h.app.Services.GetGroupFilterStats(orgID, current, filter) + stats, err := h.app.Services.GetGroupFilterStats(orgID, current, filter, false) if err != nil { log.Println(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) From b850e17c80aa04388e06fe7f75c9497cbb0ca848 Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 17 Dec 2025 12:46:39 +0200 Subject: [PATCH 17/22] fix limit --- driven/storage/adapter.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index c03485f8..3c649537 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -21,6 +21,7 @@ import ( "groups/core/model" "groups/utils" "log" + "math" "reflect" "strconv" "strings" @@ -673,7 +674,7 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode } - var limitIDRowNumber int + var limitIDRowNumber int64 if groupsFilter.LimitID != nil { var rowNumbers []rowNumber err := sa.db.groups.AggregateWithContext(ctx, bson.A{ @@ -712,6 +713,7 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode bson.D{{Key: "$skip", Value: offset}}, } if groupsFilter.Limit != nil { + limitIDRowNumber = int64(math.Max(float64(limitIDRowNumber), float64(*groupsFilter.Limit))) if limitIDRowNumber > 0 { pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(limitIDRowNumber)}}) } else { From d3be0c2cd562b11820e0da67e50d5da3d602a923 Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 17 Dec 2025 12:55:24 +0200 Subject: [PATCH 18/22] fix wrong type --- 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 3c649537..467daafa 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -659,7 +659,7 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode } type rowNumber struct { - RowNumber int `json:"_row_number" bson:"_row_number"` + RowNumber int64 `json:"_row_number" bson:"_row_number"` } var aggrSort bson.D From 8ee69aa8e8fd3b9d8f7b9abddd2ecb2f3d4bdd61 Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 17 Dec 2025 13:37:45 +0200 Subject: [PATCH 19/22] Handle additional 5 records if limit_id presents --- driven/storage/adapter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 467daafa..554823f8 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -713,8 +713,9 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode bson.D{{Key: "$skip", Value: offset}}, } if groupsFilter.Limit != nil { - limitIDRowNumber = int64(math.Max(float64(limitIDRowNumber), float64(*groupsFilter.Limit))) if limitIDRowNumber > 0 { + // Hardcoded for now... 5 extra rows to ensure we get enough results after the limitID offset. + limitIDRowNumber = int64(math.Max(float64(limitIDRowNumber+5), float64(*groupsFilter.Limit))) pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(limitIDRowNumber)}}) } else { pipeline = append(pipeline, bson.D{{Key: "$limit", Value: *groupsFilter.Limit}}) From f58b4a91f612c307474e9ef79cb85211ce5fcf1c Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 17 Dec 2025 14:12:50 +0200 Subject: [PATCH 20/22] Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e99af4..ae9f0c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [1.73.4] - 2025-12-17 +### Fixed +- USABILITY UI CleanUp: Group Filters - API improvements. Additional fixes[#613](https://github.com/rokwire/groups-building-block/issues/613) + ## [1.73.3] - 2025-12-16 ### Fixed - USABILITY UI CleanUp: Group Filters - API improvements. Update version of Golang and the external libraties. Additional fix for api doc [#613](https://github.com/rokwire/groups-building-block/issues/613) From a0f6f2fc489555928cf2e33ccd2efa0c22be37ee Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 17 Dec 2025 15:16:13 +0200 Subject: [PATCH 21/22] More improvements --- CHANGELOG.md | 5 +++++ core/model/filters.go | 47 ++++++++++++++++++++------------------- driven/storage/adapter.go | 6 ++++- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9f0c23..f4ea2e22 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.5] - 2025-12-17 +### Fixed +- USABILITY UI CleanUp: Group Filters - API improvements. Additional fixes[#613](https://github.com/rokwire/groups-building-block/issues/613) + ## [1.73.4] - 2025-12-17 ### Fixed - USABILITY UI CleanUp: Group Filters - API improvements. Additional fixes[#613](https://github.com/rokwire/groups-building-block/issues/613) diff --git a/core/model/filters.go b/core/model/filters.go index 64446bc0..91790812 100644 --- a/core/model/filters.go +++ b/core/model/filters.go @@ -17,29 +17,30 @@ type MembershipFilter struct { // GroupsFilter Wraps all possible filters for getting a group type GroupsFilter struct { - GroupIDs []string `json:"ids"` // membership id - 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 - MemberStatus []string `json:"member_status"` // member user status - Title *string `json:"title"` // group title - Category *string `json:"category"` // group category - Privacy *string `json:"privacy"` // group privacy - Tags []string `json:"tags"` // group tags - IncludeHidden *bool `json:"include_hidden"` // Include hidden groups - Hidden *bool `json:"hidden"` // Filter by hidden flag. Values: true (show only hidden), false (show only not hidden), missing - don't do any filtering on this field. - ExcludeMyGroups *bool `json:"exclude_my_groups"` // Exclude My groups - AuthmanEnabled *bool `json:"authman_enabled"` - ResearchOpen *bool `json:"research_open"` - 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 - LimitID *string `json:"limit_id"` // limit id - DaysInactive *int64 `json:"days_inactive"` - Administrative *bool `json:"administrative"` + GroupIDs []string `json:"ids"` // membership id + 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 + MemberStatus []string `json:"member_status"` // member user status + Title *string `json:"title"` // group title + Category *string `json:"category"` // group category + Privacy *string `json:"privacy"` // group privacy + Tags []string `json:"tags"` // group tags + IncludeHidden *bool `json:"include_hidden"` // Include hidden groups + Hidden *bool `json:"hidden"` // Filter by hidden flag. Values: true (show only hidden), false (show only not hidden), missing - don't do any filtering on this field. + ExcludeMyGroups *bool `json:"exclude_my_groups"` // Exclude My groups + AuthmanEnabled *bool `json:"authman_enabled"` + ResearchOpen *bool `json:"research_open"` + 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 + LimitID *string `json:"limit_id"` // limit id + LimitIDExtraRecords *int64 `json:"limit_id_extra_records"` // limit id number of extra records, default 0 + DaysInactive *int64 `json:"days_inactive"` + Administrative *bool `json:"administrative"` } // @name GroupsFilter // PostsFilter Wraps all possible filters for getting group post call diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 554823f8..c304e503 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -714,8 +714,12 @@ func (sa *Adapter) FindGroups(clientID string, userID *string, groupsFilter mode } if groupsFilter.Limit != nil { if limitIDRowNumber > 0 { + var extraRecords int64 = 0 + if groupsFilter.LimitIDExtraRecords != nil { + extraRecords = *groupsFilter.LimitIDExtraRecords + } // Hardcoded for now... 5 extra rows to ensure we get enough results after the limitID offset. - limitIDRowNumber = int64(math.Max(float64(limitIDRowNumber+5), float64(*groupsFilter.Limit))) + limitIDRowNumber = int64(math.Max(float64(limitIDRowNumber+extraRecords), float64(*groupsFilter.Limit))) pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(limitIDRowNumber)}}) } else { pipeline = append(pipeline, bson.D{{Key: "$limit", Value: *groupsFilter.Limit}}) From 8be456528c91e1778057a00069f3cbc77d16c40b Mon Sep 17 00:00:00 2001 From: Mladen Date: Wed, 17 Dec 2025 15:17:07 +0200 Subject: [PATCH 22/22] Update apidoc --- docs/docs.go | 168 +++++++++++++++++++++++++++++++++++++++++++++- docs/swagger.json | 168 +++++++++++++++++++++++++++++++++++++++++++++- docs/swagger.yaml | 126 +++++++++++++++++++++++++++++++++- 3 files changed, 459 insertions(+), 3 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 8389dfd1..2c69c36d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4532,6 +4532,168 @@ const docTemplate = `{ } } }, + "/api/v2/groups": { + "get": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsV2", + "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 - 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", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + } + } + } + }, + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsV2", + "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 - 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", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + } + } + } + } + }, "/api/v2/groups/{id}": { "get": { "security": [ @@ -5430,6 +5592,10 @@ const docTemplate = `{ "description": "limit id", "type": "string" }, + "limit_id_extra_records": { + "description": "limit id number of extra records, default 0", + "type": "integer" + }, "member_external_id": { "description": "member user external id", "type": "string" @@ -5438,7 +5604,7 @@ const docTemplate = `{ "description": "member id", "type": "string" }, - "member_statuses": { + "member_status": { "description": "member user status", "type": "array", "items": { diff --git a/docs/swagger.json b/docs/swagger.json index 1dda218d..df8ec4d5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4529,6 +4529,168 @@ } } }, + "/api/v2/groups": { + "get": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsV2", + "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 - 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", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + } + } + } + }, + "post": { + "security": [ + { + "AppUserAuth": [] + } + ], + "description": "Gives the groups list. It can be filtered by category, title and privacy. V2", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "operationId": "GetGroupsV2", + "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 - 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", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupsFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + } + } + } + } + }, "/api/v2/groups/{id}": { "get": { "security": [ @@ -5427,6 +5589,10 @@ "description": "limit id", "type": "string" }, + "limit_id_extra_records": { + "description": "limit id number of extra records, default 0", + "type": "integer" + }, "member_external_id": { "description": "member user external id", "type": "string" @@ -5435,7 +5601,7 @@ "description": "member id", "type": "string" }, - "member_statuses": { + "member_status": { "description": "member user status", "type": "array", "items": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7022e8c9..f31c82ac 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -287,13 +287,16 @@ definitions: limit_id: description: limit id type: string + limit_id_extra_records: + description: limit id number of extra records, default 0 + type: integer member_external_id: description: member user external id type: string member_id: description: member id type: string - member_statuses: + member_status: description: member user status items: type: string @@ -4154,6 +4157,127 @@ paths: - AppUserAuth: [] tags: - Client + /api/v2/groups: + get: + consumes: + - application/json + description: Gives the groups list. It can be filtered by category, title and + privacy. V2 + operationId: GetGroupsV2 + 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 - 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 + required: true + schema: + $ref: '#/definitions/GroupsFilter' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/Group' + type: array + security: + - AppUserAuth: [] + tags: + - Client + post: + consumes: + - application/json + description: Gives the groups list. It can be filtered by category, title and + privacy. V2 + operationId: GetGroupsV2 + 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 - 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 + required: true + schema: + $ref: '#/definitions/GroupsFilter' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/Group' + type: array + security: + - AppUserAuth: [] + tags: + - Client /api/v2/groups/{id}: get: consumes: