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

vocdoni / saas-backend / 25112822869

29 Apr 2026 01:49PM UTC coverage: 59.566% (+0.1%) from 59.441%
25112822869

push

github

web-flow
feat(groups): autogroup all members (#435)

* refactor(db): simplify OrganizationMemberGroups and OrganizationMemberGroup
* fix(migrations): optimize up migration and implement down migration
* fix(census): handle auto group in census creation and quota checks
* fix(db): detect duplicates when only twoFa fields are used in census validation
* fix(api): return MembersCount for auto group in single-group detail endpoint
* test(db): add regression test for twoFa-only duplicate detection in census validation

---------

Co-authored-by: emmdim <emmdim@users.noreply.github.com>

170 of 249 new or added lines in 6 files covered. (68.27%)

2 existing lines in 1 file now uncovered.

7544 of 12665 relevant lines covered (59.57%)

39.24 hits per line

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

71.56
/api/organization_groups.go
1
package api
2

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

7
        "github.com/go-chi/chi/v5"
8
        "github.com/vocdoni/saas-backend/api/apicommon"
9
        "github.com/vocdoni/saas-backend/db"
10
        "github.com/vocdoni/saas-backend/errors"
11
        "go.vocdoni.io/dvote/log"
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
//        @Param                        page        query                integer        false        "Page number (default: 1)"
26
//        @Param                        limit        query                integer        false        "Number of items per page (default: 10)"
27
//        @Success                200                {object}        apicommon.OrganizationMemberGroupsResponse
28
//        @Failure                400                {object}        errors.Error        "Invalid input data"
29
//        @Failure                401                {object}        errors.Error        "Unauthorized"
30
//        @Failure                404                {object}        errors.Error        "Organization not found"
31
//        @Failure                500                {object}        errors.Error        "Internal server error"
32
//        @Router                        /organizations/{address}/groups [get]
33
func (a *API) organizationMemberGroupsHandler(w http.ResponseWriter, r *http.Request) {
22✔
34
        // get the user from the request context
22✔
35
        user, ok := apicommon.UserFromContext(r.Context())
22✔
36
        if !ok {
22✔
37
                errors.ErrUnauthorized.Write(w)
×
38
                return
×
39
        }
×
40
        // get the organization info from the request context
41
        org, _, ok := a.organizationFromRequest(r)
22✔
42
        if !ok {
23✔
43
                errors.ErrNoOrganizationProvided.Write(w)
1✔
44
                return
1✔
45
        }
1✔
46
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
22✔
47
                // if the user is not admin or manager of the organization, return an error
1✔
48
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
49
                return
1✔
50
        }
1✔
51
        params, err := parsePaginationParams(r.URL.Query().Get(ParamPage), r.URL.Query().Get(ParamLimit))
20✔
52
        if err != nil {
20✔
53
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
54
                return
×
55
        }
×
56
        // send the organization back to the user
57
        totalItems, groups, err := a.db.OrganizationMemberGroups(org.Address, params.Page, params.Limit)
20✔
58
        if err != nil {
20✔
59
                errors.ErrGenericInternalServerError.Withf("could not get organization members: %v", err).Write(w)
×
60
                return
×
61
        }
×
62
        pagination, err := calculatePagination(params.Page, params.Limit, totalItems)
20✔
63
        if err != nil {
20✔
64
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
65
                return
×
66
        }
×
67

68
        memberGroups := apicommon.OrganizationMemberGroupsResponse{
20✔
69
                Pagination: pagination,
20✔
70
                Groups:     make([]*apicommon.OrganizationMemberGroupInfo, 0, len(groups)),
20✔
71
        }
20✔
72
        for _, group := range groups {
45✔
73
                memberGroups.Groups = append(memberGroups.Groups, &apicommon.OrganizationMemberGroupInfo{
25✔
74
                        ID:           group.ID.Hex(),
25✔
75
                        Title:        group.Title,
25✔
76
                        Description:  group.Description,
25✔
77
                        CreatedAt:    group.CreatedAt,
25✔
78
                        UpdatedAt:    group.UpdatedAt,
25✔
79
                        CensusIDs:    group.CensusIDs,
25✔
80
                        MembersCount: len(group.MemberIDs),
25✔
81
                        IsAutoGroup:  group.IsAutoGroup,
25✔
82
                })
25✔
83
        }
25✔
84
        apicommon.HTTPWriteJSON(w, memberGroups)
20✔
85
}
86

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

129
        group, err := a.db.OrganizationMemberGroup(groupID, org.Address)
11✔
130
        if err != nil {
13✔
131
                if err == db.ErrNotFound {
3✔
132
                        errors.ErrInvalidData.Withf("group not found").Write(w)
1✔
133
                        return
1✔
134
                }
1✔
135
                errors.ErrGenericInternalServerError.Withf("could not get organization member group: %v", err).Write(w)
1✔
136
                return
1✔
137
        }
138

139
        info := &apicommon.OrganizationMemberGroupInfo{
9✔
140
                ID:          group.ID.Hex(),
9✔
141
                Title:       group.Title,
9✔
142
                Description: group.Description,
9✔
143
                CensusIDs:   group.CensusIDs,
9✔
144
                CreatedAt:   group.CreatedAt,
9✔
145
                UpdatedAt:   group.UpdatedAt,
9✔
146
                IsAutoGroup: group.IsAutoGroup,
9✔
147
        }
9✔
148
        // Auto groups store no member IDs in the document; expose the live count
9✔
149
        // instead, consistent with the groups-listing response.
9✔
150
        if group.IsAutoGroup {
11✔
151
                count, err := a.db.CountOrgMembers(org.Address)
2✔
152
                if err != nil {
2✔
NEW
153
                        errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
154
                        return
×
NEW
155
                }
×
156
                info.MembersCount = int(count)
2✔
157
        } else {
7✔
158
                info.MemberIDs = group.MemberIDs
7✔
159
        }
7✔
160
        apicommon.HTTPWriteJSON(w, info)
9✔
161
}
162

163
// createOrganizationMemberGroupHandler godoc
164
//
165
//        @Summary                Create an organization member group
166
//        @Description        Create an organization member group with the given members or all members
167
//        @Description        Needs admin or manager role
168
//        @Tags                        organizations
169
//        @Accept                        json
170
//        @Produce                json
171
//        @Security                BearerAuth
172
//        @Param                        address        path                string                                                                                        true        "Organization address"
173
//        @Param                        group        body                apicommon.CreateOrganizationMemberGroupRequest        true        "Group info to create"
174
//        @Success                200                {object}        apicommon.OrganizationMemberGroupInfo
175
//        @Failure                400                {object}        errors.Error        "Invalid input data"
176
//        @Failure                401                {object}        errors.Error        "Unauthorized"
177
//        @Failure                404                {object}        errors.Error        "Organization not found"
178
//        @Failure                500                {object}        errors.Error        "Internal server error"
179
//        @Router                        /organizations/{address}/groups [post]
180
func (a *API) createOrganizationMemberGroupHandler(w http.ResponseWriter, r *http.Request) {
37✔
181
        // get the user from the request context
37✔
182
        user, ok := apicommon.UserFromContext(r.Context())
37✔
183
        if !ok {
37✔
184
                errors.ErrUnauthorized.Write(w)
×
185
                return
×
186
        }
×
187
        // get the organization info from the request context
188
        org, _, ok := a.organizationFromRequest(r)
37✔
189
        if !ok {
38✔
190
                errors.ErrNoOrganizationProvided.Write(w)
1✔
191
                return
1✔
192
        }
1✔
193
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
38✔
194
                // if the user is not admin or manager of the organization, return an error
2✔
195
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
2✔
196
                return
2✔
197
        }
2✔
198

199
        var toCreate apicommon.CreateOrganizationMemberGroupRequest
34✔
200
        if err := json.NewDecoder(r.Body).Decode(&toCreate); err != nil {
34✔
201
                errors.ErrMalformedBody.Write(w)
×
202
                return
×
203
        }
×
204

205
        var memberIDs []string
34✔
206
        var err error
34✔
207

34✔
208
        // Check if we should include all members
34✔
209
        if toCreate.IncludeAllMembers {
37✔
210
                // Get all member IDs from the database
3✔
211
                memberIDs, err = a.db.GetAllOrgMemberIDs(org.Address)
3✔
212
                if err != nil {
3✔
213
                        errors.ErrGenericInternalServerError.Withf("could not get all org member IDs: %v", err).Write(w)
×
214
                        return
×
215
                }
×
216
                log.Infow("creating group with all organization members",
3✔
217
                        "org", org.Address.Hex(),
3✔
218
                        "count", len(memberIDs),
3✔
219
                        "user", user.Email,
3✔
220
                        "title", toCreate.Title)
3✔
221
        } else {
31✔
222
                // Use the provided member IDs
31✔
223
                memberIDs = toCreate.MemberIDs
31✔
224
        }
31✔
225

226
        newMemberGroup := &db.OrganizationMemberGroup{
34✔
227
                Title:       toCreate.Title,
34✔
228
                Description: toCreate.Description,
34✔
229
                MemberIDs:   memberIDs,
34✔
230
                OrgAddress:  org.Address,
34✔
231
        }
34✔
232

34✔
233
        groupID, err := a.db.CreateOrganizationMemberGroup(newMemberGroup)
34✔
234
        if err != nil {
36✔
235
                if err == db.ErrNotFound {
2✔
236
                        errors.ErrInvalidData.Withf("organization not found").Write(w)
×
237
                        return
×
238
                }
×
239
                errors.ErrGenericInternalServerError.Withf("could not create organization member group: %v", err).Write(w)
2✔
240
                return
2✔
241
        }
242
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationMemberGroupInfo{
32✔
243
                ID: groupID,
32✔
244
        })
32✔
245
}
246

247
// updateOrganizationMemberGroupHandler godoc
248
//
249
//        @Summary                Update an organization member group
250
//        @Description        Update an organization member group changing the info, and adding or removing members
251
//        @Description        Needs admin or manager role
252
//        @Tags                        organizations
253
//        @Accept                        json
254
//        @Produce                json
255
//        @Security                BearerAuth
256
//        @Param                        address        path                string                                                                                        true        "Organization address"
257
//        @Param                        groupID        path                string                                                                                        true        "Group ID"
258
//        @Param                        group        body                apicommon.UpdateOrganizationMemberGroupsRequest        true        "Group info to update"
259
//        @Success                200                {string}        string                                                                                        "OK"
260
//        @Failure                400                {object}        errors.Error                                                                        "Invalid input data"
261
//        @Failure                401                {object}        errors.Error                                                                        "Unauthorized"
262
//        @Failure                404                {object}        errors.Error                                                                        "Organization or group not found"
263
//        @Failure                500                {object}        errors.Error                                                                        "Internal server error"
264
//        @Router                        /organizations/{address}/groups/{groupID} [put]
265
func (a *API) updateOrganizationMemberGroupHandler(w http.ResponseWriter, r *http.Request) {
7✔
266
        // get the group ID from the request path
7✔
267
        groupID := chi.URLParam(r, "groupID")
7✔
268
        if groupID == "" {
7✔
269
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
270
                return
×
271
        }
×
272
        // get the user from the request context
273
        user, ok := apicommon.UserFromContext(r.Context())
7✔
274
        if !ok {
7✔
275
                errors.ErrUnauthorized.Write(w)
×
276
                return
×
277
        }
×
278
        // get the organization info from the request context
279
        org, _, ok := a.organizationFromRequest(r)
7✔
280
        if !ok {
7✔
281
                errors.ErrNoOrganizationProvided.Write(w)
×
282
                return
×
283
        }
×
284
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
8✔
285
                // if the user is not admin or manager of the organization, return an error
1✔
286
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
287
                return
1✔
288
        }
1✔
289

290
        var toUpdate apicommon.UpdateOrganizationMemberGroupsRequest
6✔
291
        if err := json.NewDecoder(r.Body).Decode(&toUpdate); err != nil {
6✔
292
                errors.ErrMalformedBody.Write(w)
×
293
                return
×
294
        }
×
295

296
        err := a.db.UpdateOrganizationMemberGroup(
6✔
297
                groupID,
6✔
298
                org.Address,
6✔
299
                toUpdate.Title,
6✔
300
                toUpdate.Description,
6✔
301
                toUpdate.AddMembers,
6✔
302
                toUpdate.RemoveMembers,
6✔
303
        )
6✔
304
        if err != nil {
8✔
305
                switch err {
2✔
NEW
306
                case db.ErrNotFound, db.ErrInvalidData:
×
307
                        errors.ErrInvalidData.Withf("group not found").Write(w)
×
308
                case db.ErrAutoGroupMembersCannotBeModified:
1✔
309
                        errors.ErrAutoGroupMembersCannotBeModified.Write(w)
1✔
310
                default:
1✔
311
                        errors.ErrGenericInternalServerError.Withf("could not update organization member group: %v", err).Write(w)
1✔
312
                }
313
                return
2✔
314
        }
315
        apicommon.HTTPWriteOK(w)
4✔
316
}
317

318
// deleteOrganizationMemberGroupHandler godoc
319
//
320
//        @Summary                Delete an organization member group
321
//        @Description        Delete an organization member group by its ID
322
//        @Tags                        organizations
323
//        @Accept                        json
324
//        @Produce                json
325
//        @Security                BearerAuth
326
//        @Param                        address        path                string                        true        "Organization address"
327
//        @Param                        groupID        path                string                        true        "Group ID"
328
//        @Success                200                {string}        string                        "OK"
329
//        @Failure                400                {object}        errors.Error        "Invalid input data"
330
//        @Failure                401                {object}        errors.Error        "Unauthorized"
331
//        @Failure                404                {object}        errors.Error        "Organization or group not found"
332
//        @Failure                500                {object}        errors.Error        "Internal server error"
333
//        @Router                        /organizations/{address}/groups/{groupID} [delete]
334
func (a *API) deleteOrganizationMemberGroupHandler(w http.ResponseWriter, r *http.Request) {
6✔
335
        // get the member ID from the request path
6✔
336
        groupID := chi.URLParam(r, "groupID")
6✔
337
        if groupID == "" {
6✔
338
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
339
                return
×
340
        }
×
341
        // get the user from the request context
342
        user, ok := apicommon.UserFromContext(r.Context())
6✔
343
        if !ok {
6✔
344
                errors.ErrUnauthorized.Write(w)
×
345
                return
×
346
        }
×
347
        // get the organization info from the request context
348
        org, _, ok := a.organizationFromRequest(r)
6✔
349
        if !ok {
6✔
350
                errors.ErrNoOrganizationProvided.Write(w)
×
351
                return
×
352
        }
×
353
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
7✔
354
                // if the user is not admin or manager of the organization, return an error
1✔
355
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
356
                return
1✔
357
        }
1✔
358
        if err := a.db.DeleteOrganizationMemberGroup(groupID, org.Address); err != nil {
7✔
359
                switch err {
2✔
NEW
360
                case db.ErrNotFound:
×
361
                        errors.ErrInvalidData.Withf("group not found").Write(w)
×
362
                case db.ErrAutoGroupCannotBeDeleted:
1✔
363
                        errors.ErrAutoGroupCannotBeDeleted.Write(w)
1✔
364
                default:
1✔
365
                        errors.ErrGenericInternalServerError.Withf("could not delete organization member group: %v", err).Write(w)
1✔
366
                }
367
                return
2✔
368
        }
369
        apicommon.HTTPWriteOK(w)
3✔
370
}
371

372
// listOrganizationMemberGroupsHandler godoc
373
//
374
//        @Summary                Get the list of members with details of an organization member group
375
//        @Description        Get the list of members with details of an organization member group
376
//        @Description        Needs admin or manager role
377
//        @Tags                        organizations
378
//        @Accept                        json
379
//        @Produce                json
380
//        @Security                BearerAuth
381
//        @Param                        address        path                string        true        "Organization address"
382
//        @Param                        groupID        path                string        true        "Group ID"
383
//        @Param                        page        query                int                false        "Page number for pagination"
384
//        @Param                        limit        query                int                false        "Number of items per page"
385
//        @Success                200                {object}        apicommon.ListOrganizationMemberGroupResponse
386
//        @Failure                400                {object}        errors.Error        "Invalid input data"
387
//        @Failure                401                {object}        errors.Error        "Unauthorized"
388
//        @Failure                404                {object}        errors.Error        "Organization or group not found"
389
//        @Failure                500                {object}        errors.Error        "Internal server error"
390
//        @Router                        /organizations/{address}/groups/{groupID}/members [get]
391
func (a *API) listOrganizationMemberGroupsHandler(w http.ResponseWriter, r *http.Request) {
7✔
392
        // get the group ID from the request path
7✔
393
        groupID := chi.URLParam(r, "groupID")
7✔
394
        if groupID == "" {
7✔
395
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
396
                return
×
397
        }
×
398
        // get the user from the request context
399
        user, ok := apicommon.UserFromContext(r.Context())
7✔
400
        if !ok {
7✔
401
                errors.ErrUnauthorized.Write(w)
×
402
                return
×
403
        }
×
404
        // get the organization info from the request context
405
        org, _, ok := a.organizationFromRequest(r)
7✔
406
        if !ok {
7✔
407
                errors.ErrNoOrganizationProvided.Write(w)
×
408
                return
×
409
        }
×
410
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
8✔
411
                // if the user is not admin or manager of the organization, return an error
1✔
412
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
413
                return
1✔
414
        }
1✔
415

416
        params, err := parsePaginationParams(r.URL.Query().Get(ParamPage), r.URL.Query().Get(ParamLimit))
6✔
417
        if err != nil {
6✔
418
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
419
                return
×
420
        }
×
421
        totalItems, members, err := a.db.ListOrganizationMemberGroup(groupID, org.Address,
6✔
422
                params.Page, params.Limit)
6✔
423
        if err != nil {
7✔
424
                if err == db.ErrNotFound {
1✔
425
                        errors.ErrInvalidData.Withf("group not found").Write(w)
×
426
                        return
×
427
                }
×
428
                errors.ErrGenericInternalServerError.Withf("could not get organization member group members: %v", err).Write(w)
1✔
429
                return
1✔
430
        }
431
        // convert the members to the response format
432
        membersResponse := make([]apicommon.OrgMember, 0, len(members))
5✔
433
        for _, m := range members {
17✔
434
                membersResponse = append(membersResponse, apicommon.OrgMemberFromDb(*m))
12✔
435
        }
12✔
436

437
        pagination, err := calculatePagination(params.Page, params.Limit, totalItems)
5✔
438
        if err != nil {
5✔
439
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
440
                return
×
441
        }
×
442

443
        apicommon.HTTPWriteJSON(w, &apicommon.ListOrganizationMemberGroupResponse{
5✔
444
                Pagination: pagination,
5✔
445
                Members:    membersResponse,
5✔
446
        })
5✔
447
}
448

449
// organizationMemberGroupValidateHandler godoc
450
//
451
//        @Summary                Validate organization group members data
452
//        @Description        Checks the AuthFields for duplicates or empty fields and the TwoFaFields for empty ones.
453
//        @Tags                        organizations
454
//        @Accept                        json
455
//        @Produce                json
456
//        @Security                BearerAuth
457
//        @Param                        address        path                string                                                                        true        "Organization address"
458
//        @Param                        groupID        path                string                                                                        true        "Group ID"
459
//        @Param                        members        body                apicommon.ValidateMemberGroupRequest        true        "Members validation request"
460
//        @Success                200                {string}        string                                                                        "OK"
461
//        @Failure                400                {object}        errors.Error                                                        "Invalid input data"
462
//        @Failure                401                {object}        errors.Error                                                        "Unauthorized"
463
//        @Failure                404                {object}        errors.Error                                                        "Organization or group not found"
464
//        @Failure                500                {object}        errors.Error                                                        "Internal server error"
465
//
466
//        @Router                        /organizations/{address}/groups/{groupID}/validate [post]
467
func (a *API) organizationMemberGroupValidateHandler(w http.ResponseWriter, r *http.Request) {
9✔
468
        // get the group ID from the request path
9✔
469
        groupID := chi.URLParam(r, "groupID")
9✔
470
        if groupID == "" {
9✔
471
                errors.ErrInvalidData.Withf("group ID is required").Write(w)
×
472
                return
×
473
        }
×
474
        // get the user from the request context
475
        user, ok := apicommon.UserFromContext(r.Context())
9✔
476
        if !ok {
9✔
477
                errors.ErrUnauthorized.Write(w)
×
478
                return
×
479
        }
×
480
        // get the organization info from the request context
481
        org, _, ok := a.organizationFromRequest(r)
9✔
482
        if !ok {
9✔
483
                errors.ErrNoOrganizationProvided.Write(w)
×
484
                return
×
485
        }
×
486
        if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
10✔
487
                // if the user is not admin or manager of the organization, return an error
1✔
488
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
489
                return
1✔
490
        }
1✔
491

492
        var membersRequest apicommon.ValidateMemberGroupRequest
8✔
493
        if err := json.NewDecoder(r.Body).Decode(&membersRequest); err != nil {
8✔
494
                errors.ErrMalformedBody.Write(w)
×
495
                return
×
496
        }
×
497

498
        if len(membersRequest.AuthFields) == 0 && len(membersRequest.TwoFaFields) == 0 {
9✔
499
                errors.ErrInvalidData.Withf("missing both AuthFields and TwoFaFields").Write(w)
1✔
500
                return
1✔
501
        }
1✔
502

503
        // check the org members to veriy tha the OrgMemberAuthFields can be used for authentication
504
        aggregationResults, err := a.db.CheckGroupMembersFields(
7✔
505
                org.Address,
7✔
506
                groupID,
7✔
507
                membersRequest.AuthFields,
7✔
508
                membersRequest.TwoFaFields,
7✔
509
        )
7✔
510
        if err != nil {
8✔
511
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
1✔
512
                return
1✔
513
        }
1✔
514
        if len(aggregationResults.Duplicates) > 0 || len(aggregationResults.MissingData) > 0 {
9✔
515
                // if there are incorrect members, return an error with the IDs of the incorrect members
3✔
516
                errors.ErrInvalidData.WithData(aggregationResults).Write(w)
3✔
517
                return
3✔
518
        }
3✔
519

520
        apicommon.HTTPWriteOK(w)
3✔
521
}
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

© 2026 Coveralls, Inc