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

vocdoni / saas-backend / 16301607959

15 Jul 2025 06:42PM UTC coverage: 56.779% (+0.7%) from 56.082%
16301607959

Pull #165

github

emmdim
refactor:  census creation

- Removed the PublishedCensus type and added it as Census parameter.
- Introduced new OrgMemberAuthFields defining the data options for member authentication.
- Added the `CheckOrgMemberAuthFields` function that checks a set of members and given auth fields empties an
d duplicates
- Add the option to create a census through the api based on a given group
Pull Request #165: Implements group based census creation

247 of 396 new or added lines in 9 files covered. (62.37%)

4 existing lines in 3 files now uncovered.

5101 of 8984 relevant lines covered (56.78%)

25.33 hits per line

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

68.6
/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 any role in the organization.
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) {
9✔
46
        // Parse request
9✔
47
        censusInfo := &apicommon.CreateCensusRequest{}
9✔
48
        if err := json.NewDecoder(r.Body).Decode(&censusInfo); err != nil {
9✔
49
                errors.ErrMalformedBody.Write(w)
×
50
                return
×
51
        }
×
52

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

60
        // check the user has the necessary permissions
61
        if hasAnyRole, err := a.db.UserHasAnyRoleInOrg(user.Email, censusInfo.OrgAddress); err != nil {
9✔
NEW
62
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
63
                return
×
64
        } else if !hasAnyRole {
10✔
65
                // if the user does not have any role in the organization, return unauthorized
1✔
66
                errors.ErrUnauthorized.Withf("user does not have a role in for the organization").Write(w)
1✔
67
                return
1✔
68
        }
1✔
69

70
        if len(censusInfo.AuthFields) == 0 && len(censusInfo.TwoFaFields) == 0 {
9✔
71
                errors.ErrInvalidData.Withf("missing both AuthFields and TwoFaFields ").Write(w)
1✔
72
                return
1✔
73
        }
1✔
74
        // check the org members to veriy tha the OrgMemberAuthFields can be used for authentication
75
        aggregationResults, err := a.db.CheckGroupMembersFields(
7✔
76
                censusInfo.OrgAddress,
7✔
77
                censusInfo.GroupID,
7✔
78
                censusInfo.AuthFields,
7✔
79
                censusInfo.TwoFaFields,
7✔
80
        )
7✔
81
        if err != nil {
8✔
82
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
1✔
83
                return
1✔
84
        }
1✔
85
        if len(aggregationResults.Duplicates) > 0 || len(aggregationResults.Empties) > 0 {
8✔
86
                // if there are incorrect members, return an error with the IDs of the incorrect members
2✔
87
                errors.ErrInvalidCensusData.WithData(aggregationResults).Write(w)
2✔
88
                return
2✔
89
        }
2✔
90

91
        census := &db.Census{
4✔
92
                Type:       censusInfo.Type,
4✔
93
                OrgAddress: censusInfo.OrgAddress,
4✔
94
                AuthFields: censusInfo.AuthFields,
4✔
95
                CreatedAt:  time.Now(),
4✔
96
        }
4✔
97
        var censusID string
4✔
98
        if censusInfo.GroupID != "" {
5✔
99
                censusID, err = a.db.SetGroupCensus(census, censusInfo.GroupID, aggregationResults.Members)
1✔
100
        } else {
4✔
101
                censusID, err = a.db.SetCensus(census)
3✔
102
        }
3✔
103
        if err != nil {
4✔
104
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
105
                return
×
106
        }
×
107

108
        // add census members
109
        apicommon.HTTPWriteJSON(w, apicommon.CreateCensusResponse{
4✔
110
                ID: censusID,
4✔
111
        })
4✔
112
}
113

114
// censusInfoHandler godoc
115
//
116
//        @Summary                Get census information
117
//        @Description        Retrieve census information by ID. Returns census type, organization address, and creation time.
118
//        @Tags                        census
119
//        @Accept                        json
120
//        @Produce                json
121
//        @Param                        id        path                string        true        "Census ID"
122
//        @Success                200        {object}        apicommon.OrganizationCensus
123
//        @Failure                400        {object}        errors.Error        "Invalid census ID"
124
//        @Failure                404        {object}        errors.Error        "Census not found"
125
//        @Failure                500        {object}        errors.Error        "Internal server error"
126
//        @Router                        /census/{id} [get]
127
func (a *API) censusInfoHandler(w http.ResponseWriter, r *http.Request) {
2✔
128
        censusID := internal.HexBytes{}
2✔
129
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
3✔
130
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
1✔
131
                return
1✔
132
        }
1✔
133
        census, err := a.db.Census(censusID.String())
1✔
134
        if err != nil {
1✔
135
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
136
                return
×
137
        }
×
138
        apicommon.HTTPWriteJSON(w, apicommon.OrganizationCensusFromDB(census))
1✔
139
}
140

141
// addCensusParticipantsHandler godoc
142
//
143
//        @Summary                Add participants to a census
144
//        @Description        Add multiple participants to a census. Requires Manager/Admin role.
145
//        @Tags                        census
146
//        @Accept                        json
147
//        @Produce                json
148
//        @Security                BearerAuth
149
//        @Param                        id                path                string                                                true        "Census ID"
150
//        @Param                        async        query                boolean                                                false        "Process asynchronously and return job ID"
151
//        @Param                        request        body                apicommon.AddMembersRequest        true        "Participants to add"
152
//        @Success                200                {object}        apicommon.AddMembersResponse
153
//        @Failure                400                {object}        errors.Error        "Invalid input data"
154
//        @Failure                401                {object}        errors.Error        "Unauthorized"
155
//        @Failure                404                {object}        errors.Error        "Census not found"
156
//        @Failure                500                {object}        errors.Error        "Internal server error"
157
//        @Router                        /census/{id} [post]
158
func (a *API) addCensusParticipantsHandler(w http.ResponseWriter, r *http.Request) {
6✔
159
        censusID := internal.HexBytes{}
6✔
160
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
7✔
161
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
1✔
162
                return
1✔
163
        }
1✔
164
        // get the user from the request context
165
        user, ok := apicommon.UserFromContext(r.Context())
5✔
166
        if !ok {
5✔
167
                errors.ErrUnauthorized.Write(w)
×
168
                return
×
169
        }
×
170
        // get the async flag
171
        async := r.URL.Query().Get("async") == "true"
5✔
172

5✔
173
        // retrieve census
5✔
174
        census, err := a.db.Census(censusID.String())
5✔
175
        if err != nil {
5✔
176
                if err == db.ErrNotFound {
×
177
                        errors.ErrMalformedURLParam.Withf("census not found").Write(w)
×
178
                        return
×
179
                }
×
180
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
181
                return
×
182
        }
183
        // check the user has the necessary permissions
184
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
5✔
185
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
186
                return
×
187
        }
×
188
        // decode the participants from the request body
189
        members := &apicommon.AddMembersRequest{}
5✔
190
        if err := json.NewDecoder(r.Body).Decode(members); err != nil {
5✔
191
                log.Error(err)
×
192
                errors.ErrMalformedBody.Withf("missing participants").Write(w)
×
193
                return
×
194
        }
×
195
        // check if there are participants to add
196
        if len(members.Members) == 0 {
6✔
197
                apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{Count: 0})
1✔
198
                return
1✔
199
        }
1✔
200
        // add the org members as census participants in the database
201
        progressChan, err := a.db.SetBulkCensusOrgMemberParticipant(
4✔
202
                passwordSalt,
4✔
203
                censusID.String(),
4✔
204
                members.DbOrgMembers(census.OrgAddress),
4✔
205
        )
4✔
206
        if err != nil {
4✔
207
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
208
                return
×
209
        }
×
210

211
        if !async {
7✔
212
                // Wait for the channel to be closed (100% completion)
3✔
213
                var lastProgress *db.BulkCensusParticipantStatus
3✔
214
                for p := range progressChan {
9✔
215
                        lastProgress = p
6✔
216
                        // Just drain the channel until it's closed
6✔
217
                        log.Debugw("census add participants",
6✔
218
                                "census", censusID.String(),
6✔
219
                                "org", census.OrgAddress,
6✔
220
                                "progress", p.Progress,
6✔
221
                                "added", p.Added,
6✔
222
                                "total", p.Total)
6✔
223
                }
6✔
224
                // Return the number of participants added
225
                apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{Count: uint32(lastProgress.Added)})
3✔
226
                return
3✔
227
        }
228

229
        // if async create a new job identifier
230
        jobID := internal.HexBytes(util.RandomBytes(16))
1✔
231
        go func() {
2✔
232
                for p := range progressChan {
3✔
233
                        // We need to drain the channel to avoid blocking
2✔
234
                        addParticipantsToCensusWorkers.Store(jobID.String(), p)
2✔
235
                }
2✔
236
        }()
237

238
        apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{JobID: jobID})
1✔
239
}
240

241
// censusAddParticipantsJobStatusHandler godoc
242
//
243
//        @Summary                Check the progress of adding participants
244
//        @Description        Check the progress of a job to add participants to a census. Returns the progress of the job.
245
//        @Description        If the job is completed, the job is deleted after 60 seconds.
246
//        @Tags                        census
247
//        @Accept                        json
248
//        @Produce                json
249
//        @Param                        jobid        path                string        true        "Job ID"
250
//        @Success                200                {object}        db.BulkCensusParticipantStatus
251
//        @Failure                400                {object}        errors.Error        "Invalid job ID"
252
//        @Failure                404                {object}        errors.Error        "Job not found"
253
//        @Router                        /census/job/{jobid} [get]
254
func (*API) censusAddParticipantsJobStatusHandler(w http.ResponseWriter, r *http.Request) {
5✔
255
        jobID := internal.HexBytes{}
5✔
256
        if err := jobID.ParseString(chi.URLParam(r, "jobid")); err != nil {
5✔
257
                errors.ErrMalformedURLParam.Withf("invalid job ID").Write(w)
×
258
                return
×
259
        }
×
260

261
        if v, ok := addParticipantsToCensusWorkers.Load(jobID.String()); ok {
10✔
262
                p, ok := v.(*db.BulkCensusParticipantStatus)
5✔
263
                if !ok {
5✔
264
                        errors.ErrGenericInternalServerError.Withf("invalid job status type").Write(w)
×
265
                        return
×
266
                }
×
267
                if p.Progress == 100 {
6✔
268
                        go func() {
2✔
269
                                // Schedule the deletion of the job after 60 seconds
1✔
270
                                time.Sleep(60 * time.Second)
1✔
271
                                addParticipantsToCensusWorkers.Delete(jobID.String())
1✔
272
                        }()
1✔
273
                }
274
                apicommon.HTTPWriteJSON(w, p)
5✔
275
                return
5✔
276
        }
277

278
        errors.ErrJobNotFound.Withf("%s", jobID.String()).Write(w)
×
279
}
280

281
// publishCensusHandler godoc
282
//
283
//        @Summary                Publish a census for voting
284
//        @Description        Publish a census for voting. Requires Manager/Admin role. Returns published census with credentials.
285
//        @Tags                        census
286
//        @Accept                        json
287
//        @Produce                json
288
//        @Security                BearerAuth
289
//        @Param                        id        path                string        true        "Census ID"
290
//        @Success                200        {object}        apicommon.PublishedCensusResponse
291
//        @Failure                400        {object}        errors.Error        "Invalid census ID"
292
//        @Failure                401        {object}        errors.Error        "Unauthorized"
293
//        @Failure                404        {object}        errors.Error        "Census not found"
294
//        @Failure                500        {object}        errors.Error        "Internal server error"
295
//        @Router                        /census/{id}/publish [post]
296
func (a *API) publishCensusHandler(w http.ResponseWriter, r *http.Request) {
4✔
297
        censusID := internal.HexBytes{}
4✔
298
        if err := censusID.ParseString(chi.URLParam(r, "id")); err != nil {
5✔
299
                errors.ErrMalformedURLParam.Withf("wrong census ID").Write(w)
1✔
300
                return
1✔
301
        }
1✔
302

303
        // get the user from the request context
304
        user, ok := apicommon.UserFromContext(r.Context())
3✔
305
        if !ok {
3✔
306
                errors.ErrUnauthorized.Write(w)
×
307
                return
×
308
        }
×
309

310
        // retrieve census
311
        census, err := a.db.Census(censusID.String())
3✔
312
        if err != nil {
3✔
313
                errors.ErrCensusNotFound.Write(w)
×
314
                return
×
315
        }
×
316

317
        // check the user has the necessary permissions
318
        if hasAnyRole, err := a.db.UserHasAnyRoleInOrg(user.Email, census.OrgAddress); err != nil {
3✔
NEW
319
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
320
                return
×
321
        } else if !hasAnyRole {
3✔
NEW
322
                // if the user does not have any role in the organization, return unauthorized
×
323
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
324
                return
×
325
        }
×
326

327
        if len(census.Published.Root) > 0 {
3✔
NEW
328
                // if the census is already published, return the censusInfo
×
NEW
329
                apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
×
NEW
330
                        URI:  census.Published.URI,
×
NEW
331
                        Root: census.Published.Root,
×
NEW
332
                })
×
NEW
333
                return
×
NEW
334
        }
×
335

336
        // if census.Type == CensusTypeSMSOrMail || census.Type == CenT {
337
        // build the census and store it
338
        cspSignerPubKey := a.account.PubKey // TODO: use a different key based on the censusID
3✔
339
        switch census.Type {
3✔
340
        case CensusTypeSMSOrMail:
3✔
341
                census.Published.Root = cspSignerPubKey
3✔
342
                census.Published.URI = a.serverURL + "/process"
3✔
343
                census.Published.CreatedAt = time.Now()
3✔
344

345
        default:
×
346
                errors.ErrCensusTypeNotFound.Write(w)
×
347
                return
×
348
        }
349

350
        if _, err := a.db.SetCensus(census); err != nil {
3✔
351
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
352
                return
×
353
        }
×
354

355
        apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
3✔
356
                URI:  census.Published.URI,
3✔
357
                Root: cspSignerPubKey,
3✔
358
        })
3✔
359
}
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