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

vocdoni / saas-backend / 20857078305

09 Jan 2026 11:13AM UTC coverage: 62.657%. First build
20857078305

Pull #401

github

altergui
fix(csp): prevent authentication for zero-weight voters

Add validation to reject authentication attempts from users with zero
weight in weighted censuses. Users with zero weight should not be able
to participate in the voting process as they have no voting power.

Changes:
- Add check in authFirstStep to return ErrZeroWeightVoter when a user
  has zero weight in a weighted census
- Add test case "User with Phone Only" to verify zero-weight users
  cannot authenticate and receive empty auth tokens

This ensures data integrity by preventing invalid voting attempts from
users who should not have voting privileges.
Pull Request #401: Stage to Prod v2.2.1 and v2.2.2

337 of 441 new or added lines in 14 files covered. (76.42%)

6819 of 10883 relevant lines covered (62.66%)

37.3 hits per line

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

92.5
/api/api.go
1
// Package api provides the HTTP API for the Vocdoni SaaS Backend
2
//
3
//        @title                                                Vocdoni SaaS API
4
//        @version                                        1.0
5
//        @description                                API for Vocdoni SaaS Backend
6
//        @termsOfService                                http://swagger.io/terms/
7
//
8
//        @contact.name                                API Support
9
//        @contact.url                                https://vocdoni.io
10
//        @contact.email                                info@vocdoni.io
11
//
12
//        @license.name                                Apache 2.0
13
//        @license.url                                http://www.apache.org/licenses/LICENSE-2.0.html
14
//
15
//        @host                                                localhost:8080
16
//        @BasePath                                        /
17
//        @schemes                                        http https
18
//
19
//        @securityDefinitions.apikey        BearerAuth
20
//        @in                                                        header
21
//        @name                                                Authorization
22
//        @description                                Type "Bearer" followed by a space and the JWT token.
23
//
24
//        @tag.name                                        auth
25
//        @tag.description                        Authentication operations
26
//
27
//        @tag.name                                        users
28
//        @tag.description                        User management operations
29
//
30
//        @tag.name                                        organizations
31
//        @tag.description                        Organization management operations
32
//
33
//        @tag.name                                        plans
34
//        @tag.description                        Subscription plans operations
35
//
36
//        @tag.name                                        census
37
//        @tag.description                        Census management operations
38
//
39
//        @tag.name                                        process
40
//        @tag.description                        Voting process operations
41
//
42
//        @tag.name                                        storage
43
//        @tag.description                        Object storage operations
44
//
45
//        @tag.name                                        transactions
46
//        @tag.description                        Transaction signing operations
47
package api
48

49
import (
50
        "fmt"
51
        "net/http"
52
        "time"
53

54
        "github.com/go-chi/chi/v5"
55
        "github.com/go-chi/chi/v5/middleware"
56
        "github.com/go-chi/cors"
57
        "github.com/go-chi/jwtauth/v5"
58
        "github.com/vocdoni/saas-backend/account"
59
        "github.com/vocdoni/saas-backend/csp"
60
        "github.com/vocdoni/saas-backend/csp/handlers"
61
        "github.com/vocdoni/saas-backend/db"
62
        "github.com/vocdoni/saas-backend/notifications"
63
        "github.com/vocdoni/saas-backend/objectstorage"
64
        "github.com/vocdoni/saas-backend/subscriptions"
65
        "go.vocdoni.io/dvote/apiclient"
66
        "go.vocdoni.io/dvote/log"
67
)
68

69
const (
70
        jwtExpiration = 360 * time.Hour // 15 days
71
        passwordSalt  = "vocdoni365"    // salt for password hashing
72
)
73

74
type Config struct {
75
        Host        string
76
        Port        int
77
        Secret      string
78
        Chain       string
79
        DB          *db.MongoStorage
80
        Client      *apiclient.HTTPclient
81
        Account     *account.Account
82
        MailService notifications.NotificationService
83
        SMSService  notifications.NotificationService
84
        WebAppURL   string
85
        ServerURL   string
86
        // FullTransparentMode if true allows signing all transactions and does not
87
        // modify any of them.
88
        FullTransparentMode bool
89
        // Subscriptions permissions manager
90
        Subscriptions *subscriptions.Subscriptions
91
        // Object storage
92
        ObjectStorage *objectstorage.Client
93
        CSP           *csp.CSP
94
        // OAuth service URL
95
        OAuthServiceURL string
96
}
97

98
// API type represents the API HTTP server with JWT authentication capabilities.
99
type API struct {
100
        db              *db.MongoStorage
101
        auth            *jwtauth.JWTAuth
102
        host            string
103
        port            int
104
        router          *chi.Mux
105
        client          *apiclient.HTTPclient
106
        account         *account.Account
107
        mail            notifications.NotificationService
108
        sms             notifications.NotificationService
109
        secret          string
110
        webAppURL       string
111
        serverURL       string
112
        transparentMode bool
113
        subscriptions   *subscriptions.Subscriptions
114
        objectStorage   *objectstorage.Client
115
        csp             *csp.CSP
116
        oauthServiceURL string
117
        stripeHandlers  *StripeHandlers
118
}
119

120
// New creates a new API HTTP server. It does not start the server. Use Start() for that.
121
func New(conf *Config) *API {
1✔
122
        if conf == nil {
1✔
123
                return nil
×
124
        }
×
125
        // Set the ServerURL for the ObjectStorageClient
126
        if conf.ObjectStorage != nil {
1✔
127
                conf.ObjectStorage.ServerURL = conf.ServerURL
×
128
        }
×
129

130
        return &API{
1✔
131
                db:              conf.DB,
1✔
132
                auth:            jwtauth.New("HS256", []byte(conf.Secret), nil),
1✔
133
                host:            conf.Host,
1✔
134
                port:            conf.Port,
1✔
135
                client:          conf.Client,
1✔
136
                account:         conf.Account,
1✔
137
                mail:            conf.MailService,
1✔
138
                sms:             conf.SMSService,
1✔
139
                secret:          conf.Secret,
1✔
140
                webAppURL:       conf.WebAppURL,
1✔
141
                serverURL:       conf.ServerURL,
1✔
142
                transparentMode: conf.FullTransparentMode,
1✔
143
                subscriptions:   conf.Subscriptions,
1✔
144
                objectStorage:   conf.ObjectStorage,
1✔
145
                csp:             conf.CSP,
1✔
146
                oauthServiceURL: conf.OAuthServiceURL,
1✔
147
        }
1✔
148
}
149

150
// Start starts the API HTTP server (non blocking).
151
func (a *API) Start() {
1✔
152
        go func() {
2✔
153
                if err := http.ListenAndServe(fmt.Sprintf("%s:%d", a.host, a.port), a.initRouter()); err != nil {
1✔
154
                        log.Fatalf("failed to start the API server: %v", err) //revive:disable:deep-exit
×
155
                }
×
156
        }()
157
}
158

159
// router creates the router with all the routes and middleware.
160
//
161
//revive:disable:function-length
162
func (a *API) initRouter() http.Handler {
1✔
163
        // Create the router with a basic middleware stack
1✔
164
        r := chi.NewRouter()
1✔
165
        r.Use(cors.New(cors.Options{
1✔
166
                AllowedOrigins:   []string{"*"},
1✔
167
                AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
1✔
168
                AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
1✔
169
                AllowCredentials: true,
1✔
170
                MaxAge:           300, // Maximum value not ignored by any of major browsers
1✔
171
        }).Handler)
1✔
172
        r.Use(middleware.Logger)
1✔
173
        r.Use(middleware.Recoverer)
1✔
174
        r.Use(middleware.Throttle(100))
1✔
175
        r.Use(middleware.ThrottleBacklog(5000, 40000, 60*time.Second))
1✔
176
        r.Use(middleware.Timeout(45 * time.Second))
1✔
177
        // set lang param in context
1✔
178
        r.Use(a.setLang)
1✔
179

1✔
180
        a.csp.PasswordSalt = passwordSalt
1✔
181
        cspHandlers := handlers.New(a.csp, a.db)
1✔
182

1✔
183
        // Initialize Stripe service
1✔
184
        if err := a.InitializeStripeService(); err != nil {
2✔
185
                log.Errorf("failed to initialize Stripe service: %v", err)
1✔
186
                // Don't fail completely, but log the error
1✔
187
        }
1✔
188

189
        handle := func(r chi.Router, method, pattern string, h http.HandlerFunc) {
78✔
190
                log.Infow("new route", "method", method, "path", pattern)
77✔
191
                switch method {
77✔
192
                case http.MethodGet:
30✔
193
                        r.Get(pattern, h)
30✔
194
                case http.MethodPut:
10✔
195
                        r.Put(pattern, h)
10✔
196
                case http.MethodPost:
31✔
197
                        r.Post(pattern, h)
31✔
198
                case http.MethodDelete:
6✔
199
                        r.Delete(pattern, h)
6✔
NEW
200
                default:
×
NEW
201
                        log.Errorf("unsupported method %s in api initRouter", method)
×
202
                }
203
        }
204

205
        // protected routes
206
        r.Group(func(r chi.Router) {
2✔
207
                // seek, verify and validate JWT tokens
1✔
208
                r.Use(jwtauth.Verifier(a.auth))
1✔
209
                // handle valid JWT tokens
1✔
210
                r.Use(a.authenticator)
1✔
211

1✔
212
                handle(r, http.MethodPost, authRefresTokenEndpoint, a.refreshTokenHandler)
1✔
213
                handle(r, http.MethodGet, authAddressesEndpoint, a.organizationAddressesHandler)
1✔
214
                handle(r, http.MethodGet, usersMeEndpoint, a.userInfoHandler)
1✔
215
                handle(r, http.MethodPut, usersMeEndpoint, a.updateUserInfoHandler)
1✔
216
                handle(r, http.MethodPut, usersPasswordEndpoint, a.updateUserPasswordHandler)
1✔
217
                handle(r, http.MethodPost, signTxEndpoint, a.signTxHandler)
1✔
218
                handle(r, http.MethodPost, signMessageEndpoint, a.signMessageHandler)
1✔
219
                handle(r, http.MethodPost, organizationsEndpoint, a.createOrganizationHandler)
1✔
220
                handle(r, http.MethodPut, organizationEndpoint, a.updateOrganizationHandler)
1✔
221
                handle(r, http.MethodGet, organizationUsersEndpoint, a.organizationUsersHandler)
1✔
222
                handle(r, http.MethodGet, organizationSubscriptionEndpoint, a.organizationSubscriptionHandler)
1✔
223
                handle(r, http.MethodPost, organizationAddUserEndpoint, a.inviteOrganizationUserHandler)
1✔
224
                handle(r, http.MethodPut, organizationUpdateUserEndpoint, a.updateOrganizationUserHandler)
1✔
225
                handle(r, http.MethodDelete, organizationDeleteUserEndpoint, a.removeOrganizationUserHandler)
1✔
226
                handle(r, http.MethodGet, organizationCensusesEndpoint, a.organizationCensusesHandler)
1✔
227
                handle(r, http.MethodGet, organizationListProcessDraftsEndpoint, a.organizationListProcessDraftsHandler)
1✔
228
                handle(r, http.MethodGet, organizationPendingUsersEndpoint, a.pendingOrganizationUsersHandler)
1✔
229
                handle(r, http.MethodPut, organizationHandlePendingInvitationEndpoint, a.updatePendingUserInvitationHandler)
1✔
230
                handle(r, http.MethodDelete, organizationHandlePendingInvitationEndpoint, a.deletePendingUserInvitationHandler)
1✔
231
                handle(r, http.MethodGet, organizationMembersEndpoint, a.organizationMembersHandler)
1✔
232
                handle(r, http.MethodPost, organizationAddMembersEndpoint, a.addOrganizationMembersHandler)
1✔
233
                handle(r, http.MethodPut, organizationUpsertMemberEndpoint, a.upsertOrganizationMemberHandler)
1✔
234
                handle(r, http.MethodGet, organizationAddMembersJobStatusEndpoint, a.addOrganizationMembersJobStatusHandler)
1✔
235
                handle(r, http.MethodDelete, organizationDeleteMembersEndpoint, a.deleteOrganizationMembersHandler)
1✔
236
                handle(r, http.MethodPost, organizationMetaEndpoint, a.addOrganizationMetaHandler)
1✔
237
                handle(r, http.MethodPut, organizationMetaEndpoint, a.updateOrganizationMetaHandler)
1✔
238
                handle(r, http.MethodGet, organizationMetaEndpoint, a.organizationMetaHandler)
1✔
239
                handle(r, http.MethodDelete, organizationMetaEndpoint, a.deleteOrganizationMetaHandler)
1✔
240
                handle(r, http.MethodPost, organizationCreateTicketEndpoint, a.organizationCreateTicket)
1✔
241
                handle(r, http.MethodPost, organizationGroupsEndpoint, a.createOrganizationMemberGroupHandler)
1✔
242
                handle(r, http.MethodGet, organizationGroupsEndpoint, a.organizationMemberGroupsHandler)
1✔
243
                handle(r, http.MethodGet, organizationGroupEndpoint, a.organizationMemberGroupHandler)
1✔
244
                handle(r, http.MethodGet, organizationGroupMembersEndpoint, a.listOrganizationMemberGroupsHandler)
1✔
245
                handle(r, http.MethodPut, organizationGroupEndpoint, a.updateOrganizationMemberGroupHandler)
1✔
246
                handle(r, http.MethodDelete, organizationGroupEndpoint, a.deleteOrganizationMemberGroupHandler)
1✔
247
                handle(r, http.MethodPost, organizationGroupValidateEndpoint, a.organizationMemberGroupValidateHandler)
1✔
248
                handle(r, http.MethodGet, organizationJobsEndpoint, a.organizationJobsHandler)
1✔
249
                handle(r, http.MethodPost, subscriptionsCheckout, a.stripeHandlers.CreateSubscriptionCheckout)
1✔
250
                handle(r, http.MethodGet, subscriptionsCheckoutSession, a.stripeHandlers.GetCheckoutSession)
1✔
251
                handle(r, http.MethodGet, subscriptionsPortal, func(w http.ResponseWriter, r *http.Request) {
1✔
252
                        a.stripeHandlers.CreateSubscriptionPortalSession(w, r, a)
×
253
                })
×
254
                handle(r, http.MethodPost, objectStorageUploadTypedEndpoint, a.objectStorage.UploadImageWithFormHandler)
1✔
255
                handle(r, http.MethodPost, censusEndpoint, a.createCensusHandler)
1✔
256
                handle(r, http.MethodPost, censusIDEndpoint, a.addCensusParticipantsHandler)
1✔
257
                handle(r, http.MethodGet, censusAddParticipantsJobStatusEndpoint, a.censusAddParticipantsJobStatusHandler)
1✔
258
                handle(r, http.MethodPost, censusPublishEndpoint, a.publishCensusHandler)
1✔
259
                handle(r, http.MethodPost, censusGroupPublishEndpoint, a.publishCensusGroupHandler)
1✔
260
                handle(r, http.MethodGet, censusParticipantsEndpoint, a.censusParticipantsHandler)
1✔
261
                handle(r, http.MethodPost, processCreateEndpoint, a.createProcessHandler)
1✔
262
                handle(r, http.MethodPut, processEndpoint, a.updateProcessHandler)
1✔
263
                handle(r, http.MethodDelete, processEndpoint, a.deleteProcessHandler)
1✔
264
                handle(r, http.MethodPost, processBundleEndpoint, a.createProcessBundleHandler)
1✔
265
                handle(r, http.MethodPut, processBundleUpdateEndpoint, a.updateProcessBundleHandler)
1✔
266
        })
267

268
        // Public routes
269
        r.Group(func(r chi.Router) {
2✔
270
                handle(r, http.MethodGet, "/ping", func(w http.ResponseWriter, _ *http.Request) {
2✔
271
                        if _, err := w.Write([]byte(".")); err != nil {
1✔
272
                                log.Warnw("failed to write ping response", "error", err)
×
273
                        }
×
274
                })
275

276
                handle(r, http.MethodPost, authLoginEndpoint, a.authLoginHandler)
1✔
277
                handle(r, http.MethodPost, oauthLoginEndpoint, a.oauthLoginHandler)
1✔
278
                handle(r, http.MethodPost, usersEndpoint, a.registerHandler)
1✔
279
                handle(r, http.MethodPost, verifyUserEndpoint, a.verifyUserAccountHandler)
1✔
280
                handle(r, http.MethodGet, verifyUserCodeEndpoint, a.userVerificationCodeInfoHandler)
1✔
281
                handle(r, http.MethodPost, verifyUserCodeEndpoint, a.resendUserVerificationCodeHandler)
1✔
282
                handle(r, http.MethodPost, usersRecoveryPasswordEndpoint, a.recoverUserPasswordHandler)
1✔
283
                handle(r, http.MethodPost, usersResetPasswordEndpoint, a.resetUserPasswordHandler)
1✔
284
                handle(r, http.MethodGet, organizationEndpoint, a.organizationInfoHandler)
1✔
285
                handle(r, http.MethodPost, organizationAcceptUserEndpoint, a.acceptOrganizationUserInvitationHandler)
1✔
286
                handle(r, http.MethodGet, organizationRolesEndpoint, a.organizationRolesHandler)
1✔
287
                handle(r, http.MethodGet, organizationTypesEndpoint, a.organizationsTypesHandler)
1✔
288
                handle(r, http.MethodGet, plansEndpoint, a.plansHandler)
1✔
289
                handle(r, http.MethodGet, planInfoEndpoint, a.planInfoHandler)
1✔
290
                handle(r, http.MethodPost, subscriptionsWebhook, a.stripeHandlers.HandleWebhook)
1✔
291
                handle(r, http.MethodGet, objectStorageDownloadTypedEndpoint, a.objectStorage.DownloadImageInlineHandler)
1✔
292
                handle(r, http.MethodGet, censusIDEndpoint, a.censusInfoHandler)
1✔
293
                handle(r, http.MethodGet, processEndpoint, a.processInfoHandler)
1✔
294
                handle(r, http.MethodPost, processSignInfoEndpoint, cspHandlers.ConsumedAddressHandler)
1✔
295
                handle(r, http.MethodGet, processBundleInfoEndpoint, a.processBundleInfoHandler)
1✔
296
                handle(r, http.MethodPost, processBundleWeightEndpoint, cspHandlers.UserWeightHandler)
1✔
297
                handle(r, http.MethodPost, processBundleAuthEndpoint, cspHandlers.BundleAuthHandler)
1✔
298
                handle(r, http.MethodPost, processBundleSignEndpoint, cspHandlers.BundleSignHandler)
1✔
299
                handle(r, http.MethodGet, processBundleMemberEndpoint, a.processBundleParticipantInfoHandler)
1✔
300
        })
301
        a.router = r
1✔
302
        return r
1✔
303
}
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