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

vocdoni / saas-backend / 16598457312

29 Jul 2025 02:04PM UTC coverage: 58.003% (-0.02%) from 58.021%
16598457312

Pull #203

github

emmdim
api: Adds `GET /census/{id}/participants` endpoint to retrieve the memberIDs of the participants of a census
Pull Request #203: api: Adds `GET /census/{id}/participants` endpoint to retrieve the memberIDs of the participants of a census

21 of 39 new or added lines in 2 files covered. (53.85%)

2 existing lines in 1 file now uncovered.

5327 of 9184 relevant lines covered (58.0%)

26.93 hits per line

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

65.71
/api/census.go
1
package api
2

3
import (
4
        "encoding/json"
5
        "net/http"
6
        "sync"
7
        "time"
8

9
        "github.com/go-chi/chi/v5"
10
        "github.com/vocdoni/saas-backend/api/apicommon"
11
        "github.com/vocdoni/saas-backend/db"
12
        "github.com/vocdoni/saas-backend/errors"
13
        "github.com/vocdoni/saas-backend/internal"
14
        "go.vocdoni.io/dvote/log"
15
        "go.vocdoni.io/dvote/util"
16
)
17

18
const (
19
        // CensusTypeSMSOrMail is the CSP based type of census that supports both SMS and mail.
20
        CensusTypeSMSOrMail = "sms_or_mail"
21
        CensusTypeMail      = "mail"
22
        CensusTypeSMS       = "sms"
23
)
24

25
// addParticipantsToCensusWorkers is a map of job identifiers to the progress of adding participants to a census.
26
// This is used to check the progress of the job.
27
var addParticipantsToCensusWorkers sync.Map
28

29
// createCensusHandler godoc
30
//
31
//        @Summary                Create a new census
32
//        @Description        Create a new census for an organization. Requires Manager/Admin role.
33
//        @Description        Creates either a regular census or a group-based census if GroupID is provided.
34
//        @Description        Validates that either AuthFields or TwoFaFields are provided and checks for duplicates or empty fields.
35
//        @Tags                        census
36
//        @Accept                        json
37
//        @Produce                json
38
//        @Security                BearerAuth
39
//        @Param                        request        body                apicommon.CreateCensusRequest        true        "Census information"
40
//        @Success                200                {object}        apicommon.CreateCensusResponse        "Returns the created census ID"
41
//        @Failure                400                {object}        errors.Error                                        "Invalid input data or missing required fields"
42
//        @Failure                401                {object}        errors.Error                                        "Unauthorized"
43
//        @Failure                500                {object}        errors.Error                                        "Internal server error"
44
//        @Router                        /census [post]
45
func (a *API) createCensusHandler(w http.ResponseWriter, r *http.Request) {
8✔
46
        // Parse request
8✔
47
        censusInfo := &apicommon.CreateCensusRequest{}
8✔
48
        if err := json.NewDecoder(r.Body).Decode(&censusInfo); err != nil {
8✔
49
                errors.ErrMalformedBody.Write(w)
×
50
                return
×
51
        }
×
52

53
        // get the user from the request context
54
        user, ok := apicommon.UserFromContext(r.Context())
8✔
55
        if !ok {
8✔
56
                errors.ErrUnauthorized.Write(w)
×
57
                return
×
58
        }
×
59

60
        // check the user has the necessary permissions
61
        if !user.HasRoleFor(censusInfo.OrgAddress, db.ManagerRole) && !user.HasRoleFor(censusInfo.OrgAddress, db.AdminRole) {
9✔
62
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
1✔
63
                return
1✔
64
        }
1✔
65

66
        if len(censusInfo.AuthFields) == 0 && len(censusInfo.TwoFaFields) == 0 {
8✔
67
                errors.ErrInvalidData.Withf("missing both AuthFields and TwoFaFields").Write(w)
1✔
68
                return
1✔
69
        }
1✔
70

71
        census := &db.Census{
6✔
72
                OrgAddress:  censusInfo.OrgAddress,
6✔
73
                AuthFields:  censusInfo.AuthFields,
6✔
74
                TwoFaFields: censusInfo.TwoFaFields,
6✔
75
                CreatedAt:   time.Now(),
6✔
76
        }
6✔
77

6✔
78
        // In the regular census, members will be added later so we just create the DB entry
6✔
79
        censusID, err := a.db.SetCensus(census)
6✔
80
        if err != nil {
6✔
81
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
82
                return
×
83
        }
×
84

85
        apicommon.HTTPWriteJSON(w, apicommon.CreateCensusResponse{
6✔
86
                ID: censusID,
6✔
87
        })
6✔
88
}
89

90
// censusInfoHandler godoc
91
//
92
//        @Summary                Get census information
93
//        @Description        Retrieve census information by ID. Returns census type, organization address, and creation time.
94
//        @Tags                        census
95
//        @Accept                        json
96
//        @Produce                json
97
//        @Param                        id        path                string        true        "Census ID"
98
//        @Success                200        {object}        apicommon.OrganizationCensus
99
//        @Failure                400        {object}        errors.Error        "Invalid census ID"
100
//        @Failure                404        {object}        errors.Error        "Census not found"
101
//        @Failure                500        {object}        errors.Error        "Internal server error"
102
//        @Router                        /census/{id} [get]
103
func (a *API) censusInfoHandler(w http.ResponseWriter, r *http.Request) {
2✔
104
        censusID := internal.HexBytes{}
2✔
105
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
3✔
106
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
1✔
107
                return
1✔
108
        }
1✔
109
        census, err := a.db.Census(censusID.String())
1✔
110
        if err != nil {
1✔
111
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
112
                return
×
113
        }
×
114
        apicommon.HTTPWriteJSON(w, apicommon.OrganizationCensusFromDB(census))
1✔
115
}
116

117
// addCensusParticipantsHandler godoc
118
//
119
//        @Summary                Add participants to a census
120
//        @Description        Add multiple participants to a census. Requires Manager/Admin role.
121
//        @Tags                        census
122
//        @Accept                        json
123
//        @Produce                json
124
//        @Security                BearerAuth
125
//        @Param                        id                path                string                                                true        "Census ID"
126
//        @Param                        async        query                boolean                                                false        "Process asynchronously and return job ID"
127
//        @Param                        request        body                apicommon.AddMembersRequest        true        "Participants to add"
128
//        @Success                200                {object}        apicommon.AddMembersResponse
129
//        @Failure                400                {object}        errors.Error        "Invalid input data"
130
//        @Failure                401                {object}        errors.Error        "Unauthorized"
131
//        @Failure                404                {object}        errors.Error        "Census not found"
132
//        @Failure                500                {object}        errors.Error        "Internal server error"
133
//        @Router                        /census/{id} [post]
134
func (a *API) addCensusParticipantsHandler(w http.ResponseWriter, r *http.Request) {
6✔
135
        censusID := internal.HexBytes{}
6✔
136
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
7✔
137
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
1✔
138
                return
1✔
139
        }
1✔
140
        // get the user from the request context
141
        user, ok := apicommon.UserFromContext(r.Context())
5✔
142
        if !ok {
5✔
143
                errors.ErrUnauthorized.Write(w)
×
144
                return
×
145
        }
×
146
        // get the async flag
147
        async := r.URL.Query().Get("async") == "true"
5✔
148

5✔
149
        // retrieve census
5✔
150
        census, err := a.db.Census(censusID.String())
5✔
151
        if err != nil {
5✔
152
                if err == db.ErrNotFound {
×
153
                        errors.ErrMalformedURLParam.Withf("census not found").Write(w)
×
154
                        return
×
155
                }
×
156
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
157
                return
×
158
        }
159
        // check the user has the necessary permissions
160
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
5✔
161
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
162
                return
×
163
        }
×
164
        // decode the participants from the request body
165
        members := &apicommon.AddMembersRequest{}
5✔
166
        if err := json.NewDecoder(r.Body).Decode(members); err != nil {
5✔
167
                log.Error(err)
×
168
                errors.ErrMalformedBody.Withf("missing participants").Write(w)
×
169
                return
×
170
        }
×
171
        // check if there are participants to add
172
        if len(members.Members) == 0 {
6✔
173
                apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{Added: 0})
1✔
174
                return
1✔
175
        }
1✔
176
        // add the org members as census participants in the database
177
        progressChan, err := a.db.SetBulkCensusOrgMemberParticipant(
4✔
178
                passwordSalt,
4✔
179
                censusID.String(),
4✔
180
                members.DbOrgMembers(census.OrgAddress),
4✔
181
        )
4✔
182
        if err != nil {
4✔
183
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
184
                return
×
185
        }
×
186

187
        if !async {
7✔
188
                // Wait for the channel to be closed (100% completion)
3✔
189
                var lastProgress *db.BulkCensusParticipantStatus
3✔
190
                for p := range progressChan {
9✔
191
                        lastProgress = p
6✔
192
                        // Just drain the channel until it's closed
6✔
193
                        log.Debugw("census add participants",
6✔
194
                                "census", censusID.String(),
6✔
195
                                "org", census.OrgAddress,
6✔
196
                                "progress", p.Progress,
6✔
197
                                "added", p.Added,
6✔
198
                                "total", p.Total)
6✔
199
                }
6✔
200
                // Return the number of participants added
201
                apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{Added: uint32(lastProgress.Added)})
3✔
202
                return
3✔
203
        }
204

205
        // if async create a new job identifier
206
        jobID := internal.HexBytes(util.RandomBytes(16))
1✔
207
        go func() {
2✔
208
                for p := range progressChan {
3✔
209
                        // We need to drain the channel to avoid blocking
2✔
210
                        addParticipantsToCensusWorkers.Store(jobID.String(), p)
2✔
211
                }
2✔
212
        }()
213

214
        apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{JobID: jobID})
1✔
215
}
216

217
// censusAddParticipantsJobStatusHandler godoc
218
//
219
//        @Summary                Check the progress of adding participants
220
//        @Description        Check the progress of a job to add participants to a census. Returns the progress of the job.
221
//        @Description        If the job is completed, the job is deleted after 60 seconds.
222
//        @Tags                        census
223
//        @Accept                        json
224
//        @Produce                json
225
//        @Param                        jobid        path                string        true        "Job ID"
226
//        @Success                200                {object}        db.BulkCensusParticipantStatus
227
//        @Failure                400                {object}        errors.Error        "Invalid job ID"
228
//        @Failure                404                {object}        errors.Error        "Job not found"
229
//        @Router                        /census/job/{jobid} [get]
230
func (*API) censusAddParticipantsJobStatusHandler(w http.ResponseWriter, r *http.Request) {
4✔
231
        jobID := internal.HexBytes{}
4✔
232
        if err := jobID.ParseString(chi.URLParam(r, "jobid")); err != nil {
4✔
233
                errors.ErrMalformedURLParam.Withf("invalid job ID").Write(w)
×
234
                return
×
235
        }
×
236

237
        if v, ok := addParticipantsToCensusWorkers.Load(jobID.String()); ok {
8✔
238
                p, ok := v.(*db.BulkCensusParticipantStatus)
4✔
239
                if !ok {
4✔
240
                        errors.ErrGenericInternalServerError.Withf("invalid job status type").Write(w)
×
241
                        return
×
242
                }
×
243
                if p.Progress == 100 {
5✔
244
                        go func() {
2✔
245
                                // Schedule the deletion of the job after 60 seconds
1✔
246
                                time.Sleep(60 * time.Second)
1✔
247
                                addParticipantsToCensusWorkers.Delete(jobID.String())
1✔
248
                        }()
1✔
249
                }
250
                apicommon.HTTPWriteJSON(w, p)
4✔
251
                return
4✔
252
        }
253

254
        errors.ErrJobNotFound.Withf("%s", jobID.String()).Write(w)
×
255
}
256

257
// publishCensusHandler godoc
258
//
259
//        @Summary                Publish a census for voting
260
//        @Description        Publish a census for voting. Requires Manager/Admin role. Returns published census with credentials.
261
//        @Tags                        census
262
//        @Accept                        json
263
//        @Produce                json
264
//        @Security                BearerAuth
265
//        @Param                        id        path                string        true        "Census ID"
266
//        @Success                200        {object}        apicommon.PublishedCensusResponse
267
//        @Failure                400        {object}        errors.Error        "Invalid census ID"
268
//        @Failure                401        {object}        errors.Error        "Unauthorized"
269
//        @Failure                404        {object}        errors.Error        "Census not found"
270
//        @Failure                500        {object}        errors.Error        "Internal server error"
271
//        @Router                        /census/{id}/publish [post]
272
func (a *API) publishCensusHandler(w http.ResponseWriter, r *http.Request) {
4✔
273
        censusID := internal.HexBytes{}
4✔
274
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
5✔
275
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
1✔
276
                return
1✔
277
        }
1✔
278

279
        // get the user from the request context
280
        user, ok := apicommon.UserFromContext(r.Context())
3✔
281
        if !ok {
3✔
282
                errors.ErrUnauthorized.Write(w)
×
283
                return
×
284
        }
×
285

286
        // retrieve census
287
        census, err := a.db.Census(censusID.String())
3✔
288
        if err != nil {
3✔
289
                errors.ErrCensusNotFound.Write(w)
×
290
                return
×
291
        }
×
292

293
        // check the user has the necessary permissions
294
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
3✔
295
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
×
296
                return
×
297
        }
×
298

299
        if len(census.Published.Root) > 0 {
3✔
300
                // if the census is already published, return the censusInfo
×
301
                apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
×
302
                        URI:  census.Published.URI,
×
303
                        Root: census.Published.Root,
×
304
                })
×
305
                return
×
306
        }
×
307

308
        // if census.Type == CensusTypeSMSOrMail || census.Type == CenT {
309
        // build the census and store it
310
        cspSignerPubKey := a.account.PubKey // TODO: use a different key based on the censusID
3✔
311
        switch census.Type {
3✔
312
        case CensusTypeSMSOrMail, CensusTypeMail, CensusTypeSMS:
3✔
313
                census.Published.Root = cspSignerPubKey
3✔
314
                census.Published.URI = a.serverURL + "/process"
3✔
315
                census.Published.CreatedAt = time.Now()
3✔
316

317
        default:
×
318
                errors.ErrCensusTypeNotFound.Write(w)
×
319
                return
×
320
        }
321

322
        if _, err := a.db.SetCensus(census); err != nil {
3✔
323
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
324
                return
×
325
        }
×
326

327
        apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
3✔
328
                URI:  census.Published.URI,
3✔
329
                Root: cspSignerPubKey,
3✔
330
        })
3✔
331
}
332

333
// publishCensusGroupHandler godoc
334
//
335
//        @Summary                Publish a group-based census for voting
336
//        @Description        Publish a census based on a specific organization members group for voting. Requires Manager/Admin role.
337
//        @Description        Returns published census with credentials.
338
//        @Tags                        census
339
//        @Accept                        json
340
//        @Produce                json
341
//        @Security                BearerAuth
342
//        @Param                        id                path                string        true        "Census ID"
343
//        @Param                        groupId        path                string        true        "Group ID"
344
//        @Success                200                {object}        apicommon.PublishedCensusResponse
345
//        @Failure                400                {object}        errors.Error        "Invalid census ID or group ID"
346
//        @Failure                401                {object}        errors.Error        "Unauthorized"
347
//        @Failure                404                {object}        errors.Error        "Census not found"
348
//        @Failure                500                {object}        errors.Error        "Internal server error"
349
func (a *API) publishCensusGroupHandler(w http.ResponseWriter, r *http.Request) {
6✔
350
        censusID := internal.HexBytes{}
6✔
351
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
7✔
352
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
1✔
353
                return
1✔
354
        }
1✔
355

356
        groupID := internal.HexBytes{}
5✔
357
        if err := groupID.ParseString(chi.URLParam(r, "groupid")); err != nil {
6✔
358
                errors.ErrMalformedURLParam.Withf("wrong group ID").Write(w)
1✔
359
                return
1✔
360
        }
1✔
361

362
        // get the user from the request context
363
        user, ok := apicommon.UserFromContext(r.Context())
4✔
364
        if !ok {
4✔
365
                errors.ErrUnauthorized.Write(w)
×
366
                return
×
367
        }
×
368

369
        // retrieve census
370
        census, err := a.db.Census(censusID.String())
4✔
371
        if err != nil {
5✔
372
                errors.ErrCensusNotFound.Write(w)
1✔
373
                return
1✔
374
        }
1✔
375

376
        // check the user has the necessary permissions
377
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
4✔
378
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
1✔
379
                return
1✔
380
        }
1✔
381

382
        if len(census.Published.Root) > 0 {
3✔
383
                // if the census is already published, return the censusInfo
1✔
384
                apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
1✔
385
                        URI:  census.Published.URI,
1✔
386
                        Root: census.Published.Root,
1✔
387
                })
1✔
388
                return
1✔
389
        }
1✔
390

391
        // if group-based census retrieve the IDs  retrieve members and add them to the census
392
        group, err := a.db.OrganizationMemberGroup(groupID.String(), census.OrgAddress)
1✔
393
        if err != nil {
1✔
394
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
395
                return
×
396
        }
×
397
        if len(group.MemberIDs) == 0 {
1✔
398
                errors.ErrInvalidCensusData.Withf("no valid members found for the census").Write(w)
×
399
                return
×
400
        }
×
401

402
        if _, err = a.db.PopulateGroupCensus(census, group.ID.Hex(), group.MemberIDs); err != nil {
1✔
403
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
404
                return
×
405
        }
×
406

407
        // if census.Type == CensusTypeSMSOrMail || census.Type == CenT {
408
        // build the census and store it
409
        cspSignerPubKey := a.account.PubKey // TODO: use a different key based on the censusID
1✔
410
        switch census.Type {
1✔
411
        case CensusTypeSMSOrMail, CensusTypeMail, CensusTypeSMS:
1✔
412
                census.Published.Root = cspSignerPubKey
1✔
413
                census.Published.URI = a.serverURL + "/process"
1✔
414
                census.Published.CreatedAt = time.Now()
1✔
415

416
        default:
×
417
                errors.ErrCensusTypeNotFound.Write(w)
×
418
                return
×
419
        }
420

421
        if _, err := a.db.SetCensus(census); err != nil {
1✔
422
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
423
                return
×
424
        }
×
425

426
        apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
1✔
427
                URI:  census.Published.URI,
1✔
428
                Root: cspSignerPubKey,
1✔
429
        })
1✔
430
}
431

432
// censusParticipantsHandler godoc
433
//
434
//        @Summary                Get census participants
435
//        @Description        Retrieve participants of a census by ID. Requires Manager/Admin role.
436
//        @Tags                        census
437
//        @Accept                        json
438
//        @Produce                json
439
//        @Security                BearerAuth
440
//        @Param                        id        path                string        true        "Census ID"
441
//        @Success                200        {object}        apicommon.CensusParticipantsResponse
442
//        @Failure                400        {object}        errors.Error        "Invalid census ID"
443
//        @Failure                401        {object}        errors.Error        "Unauthorized"
444
//        @Failure                404        {object}        errors.Error        "Census not found"
445
//        @Failure                500        {object}        errors.Error        "Internal server error"
446
//        @Router                        /census/{id}/participants [get]
447
func (a *API) censusParticipantsHandler(w http.ResponseWriter, r *http.Request) {
1✔
448
        censusID := internal.HexBytes{}
1✔
449
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
1✔
NEW
450
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
×
NEW
451
                return
×
NEW
452
        }
×
453

454
        // retrieve census
455
        census, err := a.db.Census(censusID.String())
1✔
456
        if err != nil {
1✔
NEW
457
                if err == db.ErrNotFound {
×
NEW
458
                        errors.ErrCensusNotFound.Write(w)
×
NEW
459
                        return
×
NEW
460
                }
×
NEW
461
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
462
                return
×
463
        }
464

465
        // get the user from the request context
466
        user, ok := apicommon.UserFromContext(r.Context())
1✔
467
        if !ok {
1✔
NEW
468
                errors.ErrUnauthorized.Write(w)
×
NEW
469
                return
×
NEW
470
        }
×
471

472
        // check the user has the necessary permissions
473
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
1✔
NEW
474
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
×
NEW
475
                return
×
NEW
476
        }
×
477

478
        participants, err := a.db.CensusParticipants(censusID.String())
1✔
479
        if err != nil {
1✔
NEW
480
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
481
                return
×
NEW
482
        }
×
483
        partcipantMemberIDs := make([]string, len(participants))
1✔
484
        for i, p := range participants {
3✔
485
                partcipantMemberIDs[i] = p.ParticipantID
2✔
486
        }
2✔
487

488
        apicommon.HTTPWriteJSON(w, &apicommon.CensusParticipantsResponse{
1✔
489
                CensusID:  censusID.String(),
1✔
490
                MemberIDs: partcipantMemberIDs,
1✔
491
        })
1✔
492
}
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