• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vocdoni / saas-backend / 15452033010

04 Jun 2025 08:25PM UTC coverage: 55.908% (+1.3%) from 54.574%
15452033010

Pull #115

github

emmdim
feat: add organization member groups functionality

    - Introduced endpoints for managing organization member groups, including creation, retrieval, updating, and deletion.
    - Implemented MongoDB storage methods for organization member groups, including validation of member IDs.
    - Added types and structures for organization member groups in the database schema.
    - Created tests for organization member groups to ensure functionality and data integrity.
Pull Request #115: Adds orgParticipant Groups Entity and API

411 of 547 new or added lines in 7 files covered. (75.14%)

38 existing lines in 1 file now uncovered.

4736 of 8471 relevant lines covered (55.91%)

24.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.04
/api/organization_groups.go
1
package api
2

3
import (
4
        "encoding/json"
5
        "net/http"
6
        "strconv"
7

8
        "github.com/go-chi/chi/v5"
9
        "github.com/vocdoni/saas-backend/api/apicommon"
10
        "github.com/vocdoni/saas-backend/db"
11
        "github.com/vocdoni/saas-backend/errors"
12
)
13

14
// organizationMemberGroupsHandler godoc
15
//
16
//        @Summary                Get organization member groups
17
//        @Description        Get the list of groups and their info of the organization
18
//        @Description        Does not return the members of the groups, only the groups themselves.
19
//        @Description        Needs admin or manager role
20
//        @Tags                        organizations
21
//        @Accept                        json
22
//        @Produce                json
23
//        @Security                BearerAuth
24
//        @Param                        address        path                string        true        "Organization address"
25
//        @Success                200                {object}        apicommon.OrganizationMemberGroupsResponse
26
//        @Failure                400                {object}        errors.Error        "Invalid input data"
27
//        @Failure                401                {object}        errors.Error        "Unauthorized"
28
//        @Failure                404                {object}        errors.Error        "Organization not found"
29
//        @Failure                500                {object}        errors.Error        "Internal server error"
30
//        @Router                        /organizations/{address}/groups [get]
31
func (a *API) organizationMemberGroupsHandler(w http.ResponseWriter, r *http.Request) {
5✔
32
        // get the user from the request context
5✔
33
        user, ok := apicommon.UserFromContext(r.Context())
5✔
34
        if !ok {
5✔
NEW
35
                errors.ErrUnauthorized.Write(w)
×
NEW
36
                return
×
NEW
37
        }
×
38
        // get the organization info from the request context
39
        org, _, ok := a.organizationFromRequest(r)
5✔
40
        if !ok {
6✔
41
                errors.ErrNoOrganizationProvided.Write(w)
1✔
42
                return
1✔
43
        }
1✔
44
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
5✔
45
                // if the user is not admin or manager of the organization, return an error
1✔
46
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
47
                return
1✔
48
        }
1✔
49
        // send the organization back to the user
50
        groups, err := a.db.OrganizationMemberGroups(org.Address)
3✔
51
        if err != nil {
3✔
NEW
52
                errors.ErrGenericInternalServerError.Withf("could not get organization members: %v", err).Write(w)
×
NEW
53
                return
×
NEW
54
        }
×
55
        memberGroups := apicommon.OrganizationMemberGroupsResponse{
3✔
56
                Groups: make([]*apicommon.OrganizationMemberGroupInfo, 0, len(groups)),
3✔
57
        }
3✔
58
        for _, group := range groups {
9✔
59
                memberGroups.Groups = append(memberGroups.Groups, &apicommon.OrganizationMemberGroupInfo{
6✔
60
                        ID:           group.ID.Hex(),
6✔
61
                        Title:        group.Title,
6✔
62
                        Description:  group.Description,
6✔
63
                        CreatedAt:    group.CreatedAt,
6✔
64
                        UpdatedAt:    group.UpdatedAt,
6✔
65
                        CensusIDs:    group.CensusIDs,
6✔
66
                        MembersCount: len(group.MemberIDs),
6✔
67
                })
6✔
68
        }
6✔
69
        apicommon.HTTPWriteJSON(w, memberGroups)
3✔
70
}
71

72
// organizationMemberGroupHandler godoc
73
//
74
//        @Summary                Get the information of an organization member group
75
//        @Description        Get the information of an organization member group by its ID
76
//        @Description        Needs admin or manager role
77
//        @Tags                        organizations
78
//        @Accept                        json
79
//        @Produce                json
80
//        @Security                BearerAuth
81
//        @Param                        address        path                string        true        "Organization address"
82
//        @Param                        groupID        path                string        true        "Group ID"
83
//        @Success                200                {object}        apicommon.OrganizationMemberGroupInfo
84
//        @Failure                400                {object}        errors.Error        "Invalid input data"
85
//        @Failure                401                {object}        errors.Error        "Unauthorized"
86
//        @Failure                404                {object}        errors.Error        "Organization or group not found"
87
//        @Failure                500                {object}        errors.Error        "Internal server error"
88
//        @Router                        /organizations/{address}/groups/{groupID} [get]
89
func (a *API) organizationMemberGroupHandler(w http.ResponseWriter, r *http.Request) {
6✔
90
        // get the group ID from the request path
6✔
91
        groupID := chi.URLParam(r, "groupID")
6✔
92
        if groupID == "" {
6✔
NEW
93
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
NEW
94
                return
×
NEW
95
        }
×
96
        // get the user from the request context
97
        user, ok := apicommon.UserFromContext(r.Context())
6✔
98
        if !ok {
6✔
NEW
99
                errors.ErrUnauthorized.Write(w)
×
NEW
100
                return
×
NEW
101
        }
×
102
        // get the organization info from the request context
103
        org, _, ok := a.organizationFromRequest(r)
6✔
104
        if !ok {
6✔
NEW
105
                errors.ErrNoOrganizationProvided.Write(w)
×
NEW
106
                return
×
NEW
107
        }
×
108
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
6✔
NEW
109
                // if the user is not admin or manager of the organization, return an error
×
NEW
110
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
NEW
111
                return
×
NEW
112
        }
×
113

114
        group, err := a.db.OrganizationMemberGroup(groupID, org.Address)
6✔
115
        if err != nil {
8✔
116
                if err == db.ErrNotFound {
3✔
117
                        errors.ErrInvalidData.Withf("group not found").Write(w)
1✔
118
                        return
1✔
119
                }
1✔
120
                errors.ErrGenericInternalServerError.Withf("could not get organization member group: %v", err).Write(w)
1✔
121
                return
1✔
122
        }
123
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationMemberGroupInfo{
4✔
124
                ID:          group.ID.Hex(),
4✔
125
                Title:       group.Title,
4✔
126
                Description: group.Description,
4✔
127
                MemberIDs:   group.MemberIDs,
4✔
128
                CensusIDs:   group.CensusIDs,
4✔
129
                CreatedAt:   group.CreatedAt,
4✔
130
                UpdatedAt:   group.UpdatedAt,
4✔
131
        })
4✔
132
}
133

134
// createOrganizationMemberGroupHandler godoc
135
//
136
// @Summary                Create an organization member group
137
// @Description        Create an organization member group with the given members
138
// @Description        Needs admin or manager role
139
// @Tags                        organizations
140
// @Accept                        json
141
// @Produce                json
142
// @Security                BearerAuth
143
// @Param                        address        path                string        true        "Organization address"
144
// @Param                        group        body                apicommon.CreateOrganizationMemberGroupRequest        true        "Group info to create"
145
// @Success                200                {object}        apicommon.OrganizationMemberGroupInfo
146
// @Failure                400                {object}        errors.Error        "Invalid input data"
147
// @Failure                401                {object}        errors.Error        "Unauthorized"
148
// @Failure                404                {object}        errors.Error        "Organization not found"
149
// @Failure                500                {object}        errors.Error        "Internal server error"
150
// @Router                        /organizations/{address}/groups [post]
151
func (a *API) createOrganizationMemberGroupHandler(w http.ResponseWriter, r *http.Request) {
7✔
152
        // get the user from the request context
7✔
153
        user, ok := apicommon.UserFromContext(r.Context())
7✔
154
        if !ok {
7✔
NEW
155
                errors.ErrUnauthorized.Write(w)
×
NEW
156
                return
×
NEW
157
        }
×
158
        // get the organization info from the request context
159
        org, _, ok := a.organizationFromRequest(r)
7✔
160
        if !ok {
8✔
161
                errors.ErrNoOrganizationProvided.Write(w)
1✔
162
                return
1✔
163
        }
1✔
164
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
7✔
165
                // if the user is not admin or manager of the organization, return an error
1✔
166
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
167
                return
1✔
168
        }
1✔
169

170
        var toCreate apicommon.CreateOrganizationMemberGroupRequest
5✔
171
        if err := json.NewDecoder(r.Body).Decode(&toCreate); err != nil {
5✔
NEW
172
                errors.ErrMalformedBody.Write(w)
×
NEW
173
                return
×
NEW
174
        }
×
175

176
        newMemberGroup := &db.OrganizationMemberGroup{
5✔
177
                Title:       toCreate.Title,
5✔
178
                Description: toCreate.Description,
5✔
179
                MemberIDs:   toCreate.MemberIDs,
5✔
180
                OrgAddress:  org.Address,
5✔
181
        }
5✔
182

5✔
183
        groupID, err := a.db.CreateOrganizationMembersGroup(newMemberGroup)
5✔
184
        if err != nil {
6✔
185
                if err == db.ErrNotFound {
1✔
NEW
186
                        errors.ErrInvalidData.Withf("organization not found").Write(w)
×
NEW
187
                        return
×
NEW
188
                }
×
189
                errors.ErrGenericInternalServerError.Withf("could not create organization member group: %v", err).Write(w)
1✔
190
                return
1✔
191
        }
192
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationMemberGroupInfo{
4✔
193
                ID: groupID,
4✔
194
        })
4✔
195
}
196

197
// updateOrganizationMemberGroupHandler godoc
198
//
199
//        @Summary                Update an organization member group
200
//        @Description        Update an organization member group changing the info, and adding or removing members
201
//        @Description        Needs admin or manager role
202
//        @Tags                        organizations
203
//        @Accept                        json
204
//        @Produce                json
205
//        @Security                BearerAuth
206
//        @Param                        address        path                string        true        "Organization address"
207
//        @Param                        groupID        path                string        true        "Group ID"
208
//        @Param                        group        body                apicommon.UpdateOrganizationMemberGroupsRequest        true        "Group info to update"
209
//        @Success                200                {string}        string        "OK"
210
//        @Failure                400                {object}        errors.Error        "Invalid input data"
211
//        @Failure                401                {object}        errors.Error        "Unauthorized"
212
//        @Failure                404                {object}        errors.Error        "Organization or group not found"
213
//        @Failure                500                {object}        errors.Error        "Internal server error"
214
//        @Router                        /organizations/{address}/groups/{groupID} [put]
215
func (a *API) updateOrganizationMemberGroupHandler(w http.ResponseWriter, r *http.Request) {
5✔
216
        // get the group ID from the request path
5✔
217
        groupID := chi.URLParam(r, "groupID")
5✔
218
        if groupID == "" {
5✔
NEW
219
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
NEW
220
                return
×
NEW
221
        }
×
222
        // get the user from the request context
223
        user, ok := apicommon.UserFromContext(r.Context())
5✔
224
        if !ok {
5✔
NEW
225
                errors.ErrUnauthorized.Write(w)
×
NEW
226
                return
×
NEW
227
        }
×
228
        // get the organization info from the request context
229
        org, _, ok := a.organizationFromRequest(r)
5✔
230
        if !ok {
5✔
NEW
231
                errors.ErrNoOrganizationProvided.Write(w)
×
NEW
232
                return
×
NEW
233
        }
×
234
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
6✔
235
                // if the user is not admin or manager of the organization, return an error
1✔
236
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
237
                return
1✔
238
        }
1✔
239

240
        var toUpdate apicommon.UpdateOrganizationMemberGroupsRequest
4✔
241
        if err := json.NewDecoder(r.Body).Decode(&toUpdate); err != nil {
4✔
NEW
242
                errors.ErrMalformedBody.Write(w)
×
NEW
243
                return
×
NEW
244
        }
×
245

246
        err := a.db.UpdateOrganizationMembersGroup(
4✔
247
                groupID,
4✔
248
                org.Address,
4✔
249
                toUpdate.Title,
4✔
250
                toUpdate.Description,
4✔
251
                toUpdate.AddMembers,
4✔
252
                toUpdate.RemoveMembers,
4✔
253
        )
4✔
254
        if err != nil {
5✔
255
                if err == db.ErrNotFound {
1✔
NEW
256
                        errors.ErrInvalidData.Withf("group not found").Write(w)
×
NEW
257
                        return
×
NEW
258
                }
×
259
                errors.ErrGenericInternalServerError.Withf("could not update organization member group: %v", err).Write(w)
1✔
260
                return
1✔
261
        }
262
        apicommon.HTTPWriteOK(w)
3✔
263
}
264

265
// deleteOrganizationMemberGroupHandler godoc
266
//
267
//        @Summary                Delete an organization member group
268
//        @Description        Delete an organization member group by its ID
269
//        @Tags                        organizations
270
//        @Accept                        json
271
//        @Produce                json
272
//        @Security                BearerAuth
273
//        @Param                        address        path                string        true        "Organization address"
274
//        @Param                        groupID        path                string        true        "Group ID"
275
//        @Success                200                                {string}        string                        "OK"
276
//        @Failure                400                {object}        errors.Error        "Invalid input data"
277
//        @Failure                401                {object}        errors.Error        "Unauthorized"
278
//        @Failure                404                {object}        errors.Error        "Organization or group not found"
279
//        @Failure                500                {object}        errors.Error        "Internal server error"
280
//        @Router                        /organizations/{address}/groups/{groupID} [delete]
281
func (a *API) deleteOrganizationMemberGroupHandler(w http.ResponseWriter, r *http.Request) {
4✔
282
        // get the member ID from the request path
4✔
283
        groupID := chi.URLParam(r, "groupID")
4✔
284
        if groupID == "" {
4✔
NEW
285
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
NEW
286
                return
×
NEW
287
        }
×
288
        // get the user from the request context
289
        user, ok := apicommon.UserFromContext(r.Context())
4✔
290
        if !ok {
4✔
NEW
291
                errors.ErrUnauthorized.Write(w)
×
NEW
292
                return
×
NEW
293
        }
×
294
        // get the organization info from the request context
295
        org, _, ok := a.organizationFromRequest(r)
4✔
296
        if !ok {
4✔
NEW
297
                errors.ErrNoOrganizationProvided.Write(w)
×
NEW
298
                return
×
NEW
299
        }
×
300
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
5✔
301
                // if the user is not admin or manager of the organization, return an error
1✔
302
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
303
                return
1✔
304
        }
1✔
305
        if err := a.db.DeleteOrganizationMemberGroup(groupID, org.Address); err != nil {
4✔
306
                if err == db.ErrNotFound {
1✔
NEW
307
                        errors.ErrInvalidData.Withf("group not found").Write(w)
×
NEW
308
                        return
×
NEW
309
                }
×
310
                errors.ErrGenericInternalServerError.Withf("could not delete organization member group: %v", err).Write(w)
1✔
311
                return
1✔
312
        }
313
        apicommon.HTTPWriteOK(w)
2✔
314
}
315

316
// listOrganizationMemberGroupsHandler godoc
317
//
318
//        @Summary                Get the list of members with details of an organization member group
319
//        @Description        Get the list of members with details of an organization member group
320
//        @Description        Needs admin or manager role
321
//        @Tags                        organizations
322
//        @Accept                        json
323
//        @Produce                json
324
//        @Security                BearerAuth
325
//        @Param                        address        path                string        true        "Organization address"
326
//        @Param                        groupID        path                string        true        "Group ID"
327
//        @Param                        page        query                int                false        "Page number for pagination"
328
//        @Param                        pageSize        query        int                false        "Number of items per page"
329
//        @Success                200                {object}        apicommon.OrganizationMemberGroupMembersResponse
330
//        @Failure                400                {object}        errors.Error        "Invalid input data"
331
//        @Failure                401                {object}        errors.Error        "Unauthorized"
332
//        @Failure                404                {object}        errors.Error        "Organization or group not found"
333
//        @Failure                500                {object}        errors.Error        "Internal server error"
334
//        @Router                        /organizations/{address}/groups/{groupID}/members [get]
335
func (a *API) listOrganizationMemberGroupsHandler(w http.ResponseWriter, r *http.Request) {
4✔
336
        // get the group ID from the request path
4✔
337
        groupID := chi.URLParam(r, "groupID")
4✔
338
        if groupID == "" {
4✔
NEW
339
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
NEW
340
                return
×
NEW
341
        }
×
342
        // get the user from the request context
343
        user, ok := apicommon.UserFromContext(r.Context())
4✔
344
        if !ok {
4✔
NEW
345
                errors.ErrUnauthorized.Write(w)
×
NEW
346
                return
×
NEW
347
        }
×
348
        // get the organization info from the request context
349
        org, _, ok := a.organizationFromRequest(r)
4✔
350
        if !ok {
4✔
NEW
351
                errors.ErrNoOrganizationProvided.Write(w)
×
NEW
352
                return
×
NEW
353
        }
×
354
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
5✔
355
                // if the user is not admin or manager of the organization, return an error
1✔
356
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
357
                return
1✔
358
        }
1✔
359

360
        // Parse pagination parameters from query string
361
        page := 1      // Default page number
3✔
362
        pageSize := 10 // Default page size
3✔
363

3✔
364
        if pageStr := r.URL.Query().Get("page"); pageStr != "" {
4✔
365
                if pageVal, err := strconv.Atoi(pageStr); err == nil && pageVal > 0 {
2✔
366
                        page = pageVal
1✔
367
                }
1✔
368
        }
369

370
        if pageSizeStr := r.URL.Query().Get("pageSize"); pageSizeStr != "" {
4✔
371
                if pageSizeVal, err := strconv.Atoi(pageSizeStr); err == nil && pageSizeVal > 0 {
2✔
372
                        pageSize = pageSizeVal
1✔
373
                }
1✔
374
        }
375

376
        totalPages, members, err := a.db.ListOrganizationMemberGroup(groupID, org.Address, int64(page), int64(pageSize))
3✔
377
        if err != nil {
4✔
378
                if err == db.ErrNotFound {
1✔
NEW
379
                        errors.ErrInvalidData.Withf("group not found").Write(w)
×
NEW
380
                        return
×
NEW
381
                }
×
382
                errors.ErrGenericInternalServerError.Withf("could not get organization member group members: %v", err).Write(w)
1✔
383
                return
1✔
384
        }
385
        if totalPages == 0 {
2✔
NEW
386
                // If no members are found, return an empty response
×
NEW
387
                apicommon.HTTPWriteJSON(w, &apicommon.ListOrganizationMemberGroupResponse{
×
NEW
388
                        TotalPages:  totalPages,
×
NEW
389
                        CurrentPage: 0,
×
NEW
390
                        Members:     []apicommon.OrgParticipant{},
×
NEW
391
                })
×
NEW
392
        }
×
393
        // convert the members to the response format
394
        membersResponse := make([]apicommon.OrgParticipant, 0, len(members))
2✔
395
        for _, m := range members {
6✔
396
                membersResponse = append(membersResponse, apicommon.OrgParticipantFromDb(*m))
4✔
397
        }
4✔
398

399
        apicommon.HTTPWriteJSON(w, &apicommon.ListOrganizationMemberGroupResponse{
2✔
400
                TotalPages:  totalPages,
2✔
401
                CurrentPage: page,
2✔
402
                Members:     membersResponse,
2✔
403
        })
2✔
404
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc