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

vocdoni / saas-backend / 20485716457

24 Dec 2025 12:00PM UTC coverage: 62.582% (-0.5%) from 63.071%
20485716457

push

github

altergui
test(api): dedup code with helper funcs

6670 of 10658 relevant lines covered (62.58%)

36.03 hits per line

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

92.41
/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) {
76✔
190
                log.Infow("new route", "method", method, "path", pattern)
75✔
191
                switch method {
75✔
192
                case http.MethodGet:
30✔
193
                        r.Get(pattern, h)
30✔
194
                case http.MethodPut:
9✔
195
                        r.Put(pattern, h)
9✔
196
                case http.MethodPost:
30✔
197
                        r.Post(pattern, h)
30✔
198
                case http.MethodDelete:
6✔
199
                        r.Delete(pattern, h)
6✔
200
                default:
×
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.MethodGet, organizationAddMembersJobStatusEndpoint, a.addOrganizationMembersJobStatusHandler)
1✔
234
                handle(r, http.MethodDelete, organizationDeleteMembersEndpoint, a.deleteOrganizationMembersHandler)
1✔
235
                handle(r, http.MethodPost, organizationMetaEndpoint, a.addOrganizationMetaHandler)
1✔
236
                handle(r, http.MethodPut, organizationMetaEndpoint, a.updateOrganizationMetaHandler)
1✔
237
                handle(r, http.MethodGet, organizationMetaEndpoint, a.organizationMetaHandler)
1✔
238
                handle(r, http.MethodDelete, organizationMetaEndpoint, a.deleteOrganizationMetaHandler)
1✔
239
                handle(r, http.MethodPost, organizationCreateTicketEndpoint, a.organizationCreateTicket)
1✔
240
                handle(r, http.MethodPost, organizationGroupsEndpoint, a.createOrganizationMemberGroupHandler)
1✔
241
                handle(r, http.MethodGet, organizationGroupsEndpoint, a.organizationMemberGroupsHandler)
1✔
242
                handle(r, http.MethodGet, organizationGroupEndpoint, a.organizationMemberGroupHandler)
1✔
243
                handle(r, http.MethodGet, organizationGroupMembersEndpoint, a.listOrganizationMemberGroupsHandler)
1✔
244
                handle(r, http.MethodPut, organizationGroupEndpoint, a.updateOrganizationMemberGroupHandler)
1✔
245
                handle(r, http.MethodDelete, organizationGroupEndpoint, a.deleteOrganizationMemberGroupHandler)
1✔
246
                handle(r, http.MethodPost, organizationGroupValidateEndpoint, a.organizationMemberGroupValidateHandler)
1✔
247
                handle(r, http.MethodGet, organizationJobsEndpoint, a.organizationJobsHandler)
1✔
248
                handle(r, http.MethodPost, subscriptionsCheckout, a.stripeHandlers.CreateSubscriptionCheckout)
1✔
249
                handle(r, http.MethodGet, subscriptionsCheckoutSession, a.stripeHandlers.GetCheckoutSession)
1✔
250
                handle(r, http.MethodGet, subscriptionsPortal, func(w http.ResponseWriter, r *http.Request) {
1✔
251
                        a.stripeHandlers.CreateSubscriptionPortalSession(w, r, a)
×
252
                })
×
253
                handle(r, http.MethodPost, objectStorageUploadTypedEndpoint, a.objectStorage.UploadImageWithFormHandler)
1✔
254
                handle(r, http.MethodPost, censusEndpoint, a.createCensusHandler)
1✔
255
                handle(r, http.MethodPost, censusIDEndpoint, a.addCensusParticipantsHandler)
1✔
256
                handle(r, http.MethodGet, censusAddParticipantsJobStatusEndpoint, a.censusAddParticipantsJobStatusHandler)
1✔
257
                handle(r, http.MethodPost, censusPublishEndpoint, a.publishCensusHandler)
1✔
258
                handle(r, http.MethodPost, censusGroupPublishEndpoint, a.publishCensusGroupHandler)
1✔
259
                handle(r, http.MethodGet, censusParticipantsEndpoint, a.censusParticipantsHandler)
1✔
260
                handle(r, http.MethodPost, processCreateEndpoint, a.createProcessHandler)
1✔
261
                handle(r, http.MethodPut, processEndpoint, a.updateProcessHandler)
1✔
262
                handle(r, http.MethodDelete, processEndpoint, a.deleteProcessHandler)
1✔
263
                handle(r, http.MethodPost, processBundleEndpoint, a.createProcessBundleHandler)
1✔
264
                handle(r, http.MethodPut, processBundleUpdateEndpoint, a.updateProcessBundleHandler)
1✔
265
        })
266

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

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