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

vocdoni / saas-backend / 18531836246

15 Oct 2025 02:12PM UTC coverage: 57.067% (-1.0%) from 58.083%
18531836246

Pull #84

github

altergui
api: new handler listProcessDraftsHandler
Pull Request #84: api: support creating a draft process

26 of 114 new or added lines in 2 files covered. (22.81%)

265 existing lines in 8 files now uncovered.

5931 of 10393 relevant lines covered (57.07%)

33.96 hits per line

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

86.75
/db/types.go
1
package db
2

3
//revive:disable:max-public-structs
4

5
import (
6
        "bytes"
7
        "crypto/sha256"
8
        "encoding/json"
9
        "fmt"
10
        "slices"
11
        "time"
12

13
        "github.com/ethereum/go-ethereum/common"
14
        "github.com/vocdoni/saas-backend/internal"
15
        "go.mongodb.org/mongo-driver/bson/primitive"
16
)
17

18
type User struct {
19
        ID            uint64             `json:"id" bson:"_id"`
20
        Email         string             `json:"email" bson:"email"`
21
        Password      string             `json:"password" bson:"password"`
22
        FirstName     string             `json:"firstName" bson:"firstName"`
23
        LastName      string             `json:"lastName" bson:"lastName"`
24
        Organizations []OrganizationUser `json:"organizations" bson:"organizations"`
25
        Verified      bool               `json:"verified" bson:"verified"`
26
}
27

28
type CodeType string
29

30
type UserVerification struct {
31
        ID         uint64    `json:"id" bson:"_id"`
32
        Code       string    `json:"code" bson:"code"`
33
        Type       CodeType  `json:"type" bson:"type"`
34
        Expiration time.Time `json:"expiration" bson:"expiration"`
35
}
36

37
// TODO this is the default role function while it should be
38
// used only when it is not necessary to consult the DB
39
func (u *User) HasRoleFor(address common.Address, role UserRole) bool {
366✔
40
        for _, org := range u.Organizations {
823✔
41
                if org.Address == address &&
457✔
42
                        // Check if the role matches the organization role
457✔
43
                        string(org.Role) == string(role) {
683✔
44
                        return true
226✔
45
                }
226✔
46
        }
47
        return false
140✔
48
}
49

50
type UserRole string
51

52
type OrganizationType string
53

54
type OrganizationUser struct {
55
        Address common.Address `json:"address" bson:"_id"` // common.Address is serialized as bytes in the db
56
        Role    UserRole       `json:"role" bson:"role"`
57
}
58

59
type Organization struct {
60
        Address         common.Address           `json:"address" bson:"_id"` // common.Address is serialized as bytes in the db
61
        Website         string                   `json:"website" bson:"website"`
62
        Type            OrganizationType         `json:"type" bson:"type"`
63
        Creator         string                   `json:"creator" bson:"creator"`
64
        CreatedAt       time.Time                `json:"createdAt" bson:"createdAt"`
65
        Nonce           string                   `json:"nonce" bson:"nonce"`
66
        Size            string                   `json:"size" bson:"size"`
67
        Color           string                   `json:"color" bson:"color"`
68
        Subdomain       string                   `json:"subdomain" bson:"subdomain"`
69
        Country         string                   `json:"country" bson:"country"`
70
        Timezone        string                   `json:"timezone" bson:"timezone"`
71
        Active          bool                     `json:"active" bson:"active"`
72
        Communications  bool                     `json:"communications" bson:"communications"`
73
        TokensPurchased uint64                   `json:"tokensPurchased" bson:"tokensPurchased"`
74
        TokensRemaining uint64                   `json:"tokensRemaining" bson:"tokensRemaining"`
75
        Parent          common.Address           `json:"parent" bson:"parent"`
76
        Meta            map[string]any           `json:"meta" bson:"meta"`
77
        Subscription    OrganizationSubscription `json:"subscription" bson:"subscription"`
78
        Counters        OrganizationCounters     `json:"counters" bson:"counters"`
79
}
80

81
type PlanLimits struct {
82
        Users        int `json:"teamMembers" bson:"users"`
83
        SubOrgs      int `json:"subOrgs" bson:"subOrgs"`
84
        MaxProcesses int `json:"maxProcesses" bson:"maxProcesses"`
85
        MaxCensus    int `json:"maxCensus" bson:"maxCensus"`
86
        // Max process duration in days
87
        MaxDuration int  `json:"maxDaysDuration" bson:"maxDuration"`
88
        CustomURL   bool `json:"customURL" bson:"customURL"`
89
        Drafts      bool `json:"drafts" bson:"drafts"`
90
        CustomPlan  bool `json:"customPlan" bson:"customPlan"`
91
}
92

93
type VotingTypes struct {
94
        Single     bool `json:"single" bson:"single"`
95
        Multiple   bool `json:"multiple" bson:"multiple"`
96
        Approval   bool `json:"approval" bson:"approval"`
97
        Cumulative bool `json:"cumulative" bson:"cumulative"`
98
        Ranked     bool `json:"ranked" bson:"ranked"`
99
        Weighted   bool `json:"weighted" bson:"weighted"`
100
}
101

102
type Features struct {
103
        Anonymous       bool `json:"anonymous" bson:"anonymous"`
104
        Overwrite       bool `json:"overwrite" bson:"overwrite"`
105
        LiveResults     bool `json:"liveResults" bson:"liveResults"`
106
        Personalization bool `json:"personalization" bson:"personalization"`
107
        EmailReminder   bool `json:"emailReminder" bson:"emailReminder"`
108
        TwoFaSms        int  `json:"2FAsms" bson:"twoFaSms"`
109
        TwoFaEmail      int  `json:"2FAemail" bson:"twoFaEmail"`
110
        WhiteLabel      bool `json:"whiteLabel" bson:"whiteLabel"`
111
        LiveStreaming   bool `json:"liveStreaming" bson:"liveStreaming"`
112
        PhoneSupport    bool `json:"phoneSupport" bson:"phoneSupport"`
113
}
114

115
type Plan struct {
116
        ID            uint64      `json:"id" bson:"_id"`
117
        Name          string      `json:"name" bson:"name"`
118
        StripeID      string      `json:"stripeID" bson:"stripeID"`
119
        StripePriceID string      `json:"stripePriceID" bson:"stripePriceID"`
120
        StartingPrice int64       `json:"startingPrice" bson:"startingPrice"`
121
        Default       bool        `json:"default" bson:"default"`
122
        Organization  PlanLimits  `json:"organization" bson:"organization"`
123
        VotingTypes   VotingTypes `json:"votingTypes" bson:"votingTypes"`
124
        Features      Features    `json:"features" bson:"features"`
125
}
126

127
type OrganizationSubscription struct {
128
        PlanID          uint64    `json:"planID" bson:"planID"`
129
        StartDate       time.Time `json:"startDate" bson:"startDate"`
130
        RenewalDate     time.Time `json:"renewalDate" bson:"renewalDate"`
131
        LastPaymentDate time.Time `json:"lastPaymentDate" bson:"lastPaymentDate"`
132
        Active          bool      `json:"active" bson:"active"`
133
        Email           string    `json:"email" bson:"email"`
134
}
135

136
type OrganizationCounters struct {
137
        SentSMS    int `json:"sentSMS" bson:"sentSMS"`
138
        SentEmails int `json:"sentEmails" bson:"sentEmails"`
139
        SubOrgs    int `json:"subOrgs" bson:"subOrgs"`
140
        Users      int `json:"users" bson:"users"`
141
        Processes  int `json:"processes" bson:"processes"`
142
}
143

144
type OrganizationInvite struct {
145
        ID                  primitive.ObjectID `json:"id" bson:"_id"`
146
        InvitationCode      string             `json:"invitationCode" bson:"invitationCode"`
147
        OrganizationAddress common.Address     `json:"organizationAddress" bson:"organizationAddress"`
148
        CurrentUserID       uint64             `json:"currentUserID" bson:"currentUserID"`
149
        NewUserEmail        string             `json:"newUserEmail" bson:"newUserEmail"`
150
        Role                UserRole           `json:"role" bson:"role"`
151
        Expiration          time.Time          `json:"expiration" bson:"expiration"`
152
}
153

154
// Object represents a user uploaded object Includes user defined ID and the data
155
// as a byte array.
156
type Object struct {
157
        ID          string    `json:"id" bson:"_id"`
158
        Name        string    `json:"name" bson:"name"`
159
        Data        []byte    `json:"data" bson:"data" swaggertype:"string" format:"base64" example:"aGVsbG8gd29ybGQ="`
160
        CreatedAt   time.Time `json:"createdAt" bson:"createdAt"`
161
        UserID      string    `json:"userId" bson:"userId"`
162
        ContentType string    `json:"contentType" bson:"contentType"`
163
}
164

165
// CensusType defines the type of census.
166
type CensusType string
167

168
const (
169
        // CensusTypeMail is used when the organizer uploads a list of names, memberIDs and e‑mails.
170
        CensusTypeAuthOnly  CensusType = "auth"
171
        CensusTypeMail      CensusType = "mail"
172
        CensusTypeSMS       CensusType = "sms"
173
        CensusTypeSMSorMail CensusType = "sms_or_mail"
174
)
175

176
// Census represents the information of a set of census participants
177
type Census struct {
178
        ID          primitive.ObjectID   `json:"id" bson:"_id"`
179
        OrgAddress  common.Address       `json:"orgAddress" bson:"orgAddress"`
180
        Type        CensusType           `json:"type" bson:"type"`
181
        Size        int64                `json:"size" bson:"size"`
182
        GroupID     primitive.ObjectID   `json:"groupId" bson:"groupId"`
183
        Published   PublishedCensus      `json:"published" bson:"published"`
184
        AuthFields  OrgMemberAuthFields  `json:"authFields" bson:"orgMemberAuthFields"`
185
        TwoFaFields OrgMemberTwoFaFields `json:"twoFaFields" bson:"orgMemberTwoFaFields"`
186

187
        CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
188
        UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
189
}
190

191
// An org member belongs to an organization and her details that will be
192
// used for verification and/or authentication
193
// A member is tied to an organization by the orgAddress
194
//
195
//nolint:lll
196
type OrgMember struct {
197
        // Also referred to as member UID
198
        ID primitive.ObjectID `json:"id" bson:"_id"`
199
        // OrgAddress can be used for future sharding
200
        OrgAddress     common.Address `json:"orgAddress" bson:"orgAddress"`
201
        Email          string         `json:"email" bson:"email"`
202
        Phone          HashedPhone    `json:"phone" bson:"phone"`
203
        PlaintextPhone string         `json:"-" bson:"-"`
204
        MemberNumber   string         `json:"memberNumber" bson:"memberNumber"`
205
        NationalID     string         `json:"nationalID" bson:"nationalID"`
206
        Name           string         `json:"name" bson:"name"`
207
        Surname        string         `json:"surname" bson:"surname"`
208
        BirthDate      string         `json:"birthDate" bson:"birthDate"`
209
        ParsedBirtDate time.Time      `json:"parsedBirthDate" bson:"parsedBirthDate"`
210
        Password       string         `json:"password" bson:"password"`
211
        HashedPass     []byte         `json:"pass" bson:"pass" swaggertype:"string" format:"base64" example:"aGVsbG8gd29ybGQ="`
212
        Other          map[string]any `json:"other" bson:"other"`
213
        CreatedAt      time.Time      `json:"createdAt" bson:"createdAt"`
214
        UpdatedAt      time.Time      `json:"updatedAt" bson:"updatedAt"`
215
}
216

217
// OrgMemberAuthFields defines the fields that can be used for member authentication.
218
type OrgMemberAuthField string
219

220
const (
221
        OrgMemberAuthFieldsName         OrgMemberAuthField = "name"
222
        OrgMemberAuthFieldsSurname      OrgMemberAuthField = "surname"
223
        OrgMemberAuthFieldsMemberNumber OrgMemberAuthField = "memberNumber"
224
        OrgMemberAuthFieldsNationalID   OrgMemberAuthField = "nationalId"
225
        OrgMemberAuthFieldsBirthDate    OrgMemberAuthField = "birthDate"
226
)
227

228
// OrgMemberAuthFields is a list of fields that can be used for member authentication.
229
type OrgMemberAuthFields []OrgMemberAuthField
230

231
// OrgMemberTwoFaField defines the fields that can be used for two-factor authentication.
232
type OrgMemberTwoFaField string
233

234
const (
235
        OrgMemberTwoFaFieldEmail OrgMemberTwoFaField = "email"
236
        OrgMemberTwoFaFieldPhone OrgMemberTwoFaField = "phone"
237
)
238

239
// OrgMemberTwoFaFields is a list of fields that can be used for two-factor authentication.
240
type OrgMemberTwoFaFields []OrgMemberTwoFaField
241

242
// Contains checks if the field is present in the OrgMemberAuthFields.
243
func (f OrgMemberTwoFaFields) Contains(field OrgMemberTwoFaField) bool {
244
        for _, v := range f {
245
                if v == field {
246
                        return true
247
                }
248
        }
249
        return false
250
}
251

252
func (f OrgMemberTwoFaFields) GetCensusType() CensusType {
253
        if f.Contains(OrgMemberTwoFaFieldPhone) && f.Contains(OrgMemberTwoFaFieldEmail) {
254
                return CensusTypeSMSorMail
255
        } else if f.Contains(OrgMemberTwoFaFieldPhone) {
256
                return CensusTypeSMS
257
        } else if f.Contains(OrgMemberTwoFaFieldEmail) {
189✔
258
                return CensusTypeMail
324✔
259
        }
189✔
260
        return CensusTypeAuthOnly
54✔
261
}
54✔
262

263
// HashAuthTwoFaFields helper function receives as input the data of a member and
135✔
264
// the auth and twoFa field and produces a sha256 hash of the concatenation of the
265
// data that are included in the fields. The data are ordered by the field names
266
// in order to make the hash reproducible.
64✔
267
func HashAuthTwoFaFields(memberData OrgMember, authFields OrgMemberAuthFields, twoFaFields OrgMemberTwoFaFields) []byte {
67✔
268
        data := make([]string, 0, len(twoFaFields)+len(authFields))
3✔
269
        for _, field := range authFields {
70✔
270
                switch field {
6✔
271
                case OrgMemberAuthFieldsName:
97✔
272
                        data = append(data, memberData.Name)
36✔
273
                case OrgMemberAuthFieldsSurname:
36✔
274
                        data = append(data, memberData.Surname)
19✔
275
                case OrgMemberAuthFieldsMemberNumber:
276
                        data = append(data, memberData.MemberNumber)
277
                case OrgMemberAuthFieldsNationalID:
278
                        data = append(data, memberData.NationalID)
279
                case OrgMemberAuthFieldsBirthDate:
280
                        data = append(data, memberData.BirthDate)
281
                default:
108✔
282
                        // Ignore unknown fields
108✔
283
                        continue
359✔
284
                }
251✔
285
        }
94✔
286
        for _, field := range twoFaFields {
94✔
287
                switch field {
52✔
288
                case OrgMemberTwoFaFieldEmail:
52✔
289
                        data = append(data, memberData.Email)
83✔
290
                case OrgMemberTwoFaFieldPhone:
83✔
291
                        if !memberData.Phone.IsEmpty() {
11✔
292
                                data = append(data, string(memberData.Phone))
11✔
293
                        }
11✔
294
                default:
11✔
295
                        // Ignore unknown fields
×
296
                        continue
×
UNCOV
297
                }
×
298
        }
299
        slices.Sort(data)
300
        return sha256.New().Sum(fmt.Append(nil, data))
220✔
301
}
112✔
302

82✔
303
type OrgMemberAggregationResults struct {
82✔
304
        // MemberIDs is a list of member IDs that are result of the aggregation
30✔
305
        Members []primitive.ObjectID `json:"memberIds" bson:"memberIds"`
55✔
306
        // Duplicates is a list of member IDs that were found to be duplicates
25✔
307
        Duplicates []primitive.ObjectID `json:"duplicates" bson:"duplicates"`
25✔
UNCOV
308
        // MissingData is a list of member IDs that had columns found to be empty
×
UNCOV
309
        MissingData []primitive.ObjectID `json:"missingData" bson:"missingData"`
×
UNCOV
310
}
×
311

312
// An Organization members' group is a precursor of a census, and is simply a
313
// collection of members that are grouped together for a specific purpose
108✔
314
type OrganizationMemberGroup struct {
108✔
315
        ID          primitive.ObjectID `json:"id" bson:"_id"`
316
        OrgAddress  common.Address     `json:"orgAddress" bson:"orgAddress"`
317
        Title       string             `json:"title" bson:"title"`
318
        Description string             `json:"description" bson:"description"`
319
        MemberIDs   []string           `json:"memberIds" bson:"memberIds"`
320
        CreatedAt   time.Time          `json:"createdAt" bson:"createdAt"`
321
        UpdatedAt   time.Time          `json:"updatedAt" bson:"updatedAt"`
322
        CensusIDs   []string           `json:"censusIds" bson:"censusIds"`
323
}
324

325
// Relates an OrgMember to a Census
326
// The censusID is the hex format in string of the objectID
327
//
328
//nolint:lll
329
type CensusParticipant struct {
330
        ParticipantID  string    `json:"participantID" bson:"participantID"`
331
        CensusID       string    `json:"censusId" bson:"censusId"`
332
        LoginHash      []byte    `json:"loginHash" bson:"loginHash" swaggertype:"string" format:"base64" example:"aGVsbG8gd29ybGQ="`
333
        LoginHashPhone []byte    `json:"loginHashPhone" bson:"loginHashPhone" swaggertype:"string" format:"base64" example:"aGVsbG8gd29ybGQ="`
334
        LoginHashEmail []byte    `json:"loginHashEmail" bson:"loginHashEmail" swaggertype:"string" format:"base64" example:"aGVsbG8gd29ybGQ="`
335
        CreatedAt      time.Time `json:"createdAt" bson:"createdAt"`
336
        UpdatedAt      time.Time `json:"updatedAt" bson:"updatedAt"`
337
}
338

339
// Represents a published census as a census is represented in the vochain
340
// The publishedCensus is tied to a Census
341
type PublishedCensus struct {
342
        URI       string            `json:"uri" bson:"uri"`
343
        Root      internal.HexBytes `json:"root" bson:"root"`
344
        CreatedAt time.Time         `json:"createdAt" bson:"createdAt"`
345
}
346

347
// Process represents a voting process in the vochain
348
// A process is tied to an organization by the orgAddress
349
// and to a publishedCensus
350
//
351
//nolint:lll
352
type Process struct {
353
        ID         internal.HexBytes `json:"id" bson:"_id"  swaggertype:"string" format:"hex" example:"deadbeef"`
354
        OrgAddress common.Address    `json:"orgAdress" bson:"orgAddress"`
355
        Census     Census            `json:"census" bson:"census"`
356
        Metadata   []byte            `json:"metadata,omitempty"  bson:"metadata"  swaggertype:"string" format:"base64" example:"aGVsbG8gd29ybGQ="`
357
        Draft      bool              `json:"draft" bson:"draft"`
358
}
359

360
// ProcessesBundle represents a group of voting processes that share a common census.
361
// This allows users to participate in multiple voting processes using the same authentication mechanism.
362
//
363
//nolint:lll
364
type ProcessesBundle struct {
365
        ID         primitive.ObjectID  `json:"id" bson:"_id"`                                                                         // Unique identifier for the bundle
366
        Census     Census              `json:"census" bson:"census"`                                                                  // The census associated with this bundle
367
        OrgAddress common.Address      `json:"orgAddress" bson:"orgAddress"`                                                          // The organization that owns this bundle
368
        Processes  []internal.HexBytes `json:"processes" bson:"processes" swaggertype:"array,string" format:"hex" example:"deadbeef"` // Array of process IDs included in this bundle
369
}
370

371
// HashedPhone represents a hashed phone number for database storage
372
type HashedPhone []byte
373

374
// NewHashedPhone creates a HashedPhone from a phone and organization.
375
// If phone is the empty string, returns an empty Phone and no error.
376
func NewHashedPhone(phone string, organization *Organization) (HashedPhone, error) {
377
        if phone == "" {
378
                return HashedPhone{}, nil
379
        }
380

381
        // Normalize and hash in one operation
382
        normalized, err := internal.SanitizeAndVerifyPhoneNumber(phone, organization.Country)
383
        if err != nil {
384
                return nil, err
385
        }
386
        return HashedPhone(internal.HashOrgData(organization.Address, normalized)), nil
387
}
388

389
// Matches checks if a HashedPhone matches another phone
390
func (hp HashedPhone) Matches(other HashedPhone) bool {
87✔
391
        return bytes.Equal(hp, other)
104✔
392
}
17✔
393

17✔
394
// String returns masked hash for display
395
func (hp HashedPhone) String() string {
396
        if len(hp) == 0 {
70✔
397
                return ""
72✔
398
        }
2✔
399
        hexHash := fmt.Sprintf("%x", []byte(hp))
2✔
400
        if len(hexHash) < 6 {
68✔
401
                return hexHash
402
        }
403
        return hexHash[:6] + "***"
404
}
6✔
405

6✔
406
// Bytes returns the raw bytes
6✔
407
func (hp HashedPhone) Bytes() []byte {
408
        return hp
409
}
101✔
410

123✔
411
// IsEmpty returns true if hash is empty
22✔
412
func (hp HashedPhone) IsEmpty() bool {
22✔
413
        return len(hp) == 0
79✔
414
}
79✔
UNCOV
415

×
UNCOV
416
// MarshalJSON implements the json.Marshaler interface (returns display version)
×
417
func (hp HashedPhone) MarshalJSON() ([]byte, error) {
79✔
418
        return json.Marshal(hp.String())
419
}
420

421
// JobType represents the type of import job
9✔
422
type JobType string
9✔
423

9✔
424
const (
425
        // JobTypeOrgMembers represents organization member import jobs
426
        JobTypeOrgMembers JobType = "org_members"
45✔
427
        // JobTypeCensusParticipants represents census participant import jobs
45✔
428
        JobTypeCensusParticipants JobType = "census_participants"
45✔
429
)
430

UNCOV
431
// Job represents a persistent import job with its results and errors.
×
UNCOV
432
// This allows clients to query job status and errors even after server restarts.
×
UNCOV
433
type Job struct {
×
434
        ID          primitive.ObjectID `json:"id" bson:"_id"`
435
        JobID       string             `json:"jobId" bson:"jobId"`           // The hex job ID
436
        Type        JobType            `json:"type" bson:"type"`             // Job type constant
437
        OrgAddress  common.Address     `json:"orgAddress" bson:"orgAddress"` // For authorization
438
        Total       int                `json:"total" bson:"total"`           // Total items processed
439
        Added       int                `json:"added" bson:"added"`           // Items successfully added
440
        Errors      []string           `json:"errors" bson:"errors"`         // All errors encountered
441
        CreatedAt   time.Time          `json:"createdAt" bson:"createdAt"`
442
        CompletedAt time.Time          `json:"completedAt" bson:"completedAt"`
443
}
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