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

vocdoni / saas-backend / 21478422804

29 Jan 2026 12:37PM UTC coverage: 63.363% (+0.2%) from 63.181%
21478422804

push

github

emmdim
refactor(oauth): allow multiple providers

- Adds a migration to make the user.password field non mandatory
- Also creates the "oauth" object field for any existing user
- The oauth login/signup endpoint now expects a "provider" key with the
  provider used (either google, facebook or github right now)
- Added some extra useful information to the provider because why not
- The down migration takes into consideration possible existing OAuth
  users with the new format, moving their new password to the old
  "password" field, to give some kind of backwards compatibility (and
  also to not break the migrations)

refs #286

chore(oauth): upgrade migration ids

refs #286

feat(oauth): add link/unlink endpoints

- Added a hasPassword field to the profile (/me) response
- Also added a providers field to the same profile response with an
  array of already linked providers

refs #286

chore(swagger): update

refs #286

chore(types): don't omit providers, it's useful as an empty array

refs #286

chore(test): fix test anti-pattern

refs #286

chore(user): has password field should not be omited

refs #286

fix(oauth): properly check error before defering

refs #286

fix(oauth): disallow linking already linked accounts

refs #286

refs #286

refs #286

refs #286

refs #286

refs #286

refs #286

236 of 342 new or added lines in 6 files covered. (69.01%)

12 existing lines in 1 file now uncovered.

7122 of 11240 relevant lines covered (63.36%)

37.82 hits per line

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

41.0
/api/users.go
1
package api
2

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

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

17
// registerHandler godoc
18
//
19
//        @Summary                Register a new user
20
//        @Description        Register a new user with email, password, and personal information
21
//        @Tags                        users
22
//        @Accept                        json
23
//        @Produce                json
24
//        @Param                        request        body                apicommon.UserInfo        true        "User registration information"
25
//        @Success                200                {string}        string                                "OK"
26
//        @Failure                400                {object}        errors.Error                "Invalid input data"
27
//        @Failure                409                {object}        errors.Error                "User already exists"
28
//        @Failure                500                {object}        errors.Error                "Internal server error"
29
//        @Router                        /users [post]
30
func (a *API) registerHandler(w http.ResponseWriter, r *http.Request) {
82✔
31
        userInfo := &apicommon.UserInfo{}
82✔
32
        body, err := io.ReadAll(r.Body)
82✔
33
        if err != nil {
82✔
34
                errors.ErrMalformedBody.Write(w)
×
35
                return
×
36
        }
×
37
        if err := json.Unmarshal(body, userInfo); err != nil {
83✔
38
                errors.ErrMalformedBody.Write(w)
1✔
39
                return
1✔
40
        }
1✔
41
        // check the email is correct format
42
        if !internal.ValidEmail(userInfo.Email) {
83✔
43
                errors.ErrEmailMalformed.Write(w)
2✔
44
                return
2✔
45
        }
2✔
46
        // check the password is correct format
47
        if len(userInfo.Password) < 8 {
81✔
48
                errors.ErrPasswordTooShort.Write(w)
2✔
49
                return
2✔
50
        }
2✔
51
        // check the first name is not empty
52
        if userInfo.FirstName == "" {
78✔
53
                errors.ErrMalformedBody.Withf("first name is empty").Write(w)
1✔
54
                return
1✔
55
        }
1✔
56
        // check the last name is not empty
57
        if userInfo.LastName == "" {
77✔
58
                errors.ErrMalformedBody.Withf("last name is empty").Write(w)
1✔
59
                return
1✔
60
        }
1✔
61
        // hash the password
62
        hPassword := internal.HexHashPassword(passwordSalt, userInfo.Password)
75✔
63
        // add the user to the database
75✔
64
        userID, err := a.db.SetUser(&db.User{
75✔
65
                Email:     userInfo.Email,
75✔
66
                FirstName: userInfo.FirstName,
75✔
67
                LastName:  userInfo.LastName,
75✔
68
                Password:  hPassword,
75✔
69
        })
75✔
70
        if err != nil {
76✔
71
                if err == db.ErrAlreadyExists {
2✔
72
                        errors.ErrDuplicateConflict.With("user already exists").Write(w)
1✔
73
                        return
1✔
74
                }
1✔
75
                log.Warnw("could not create user", "error", err)
×
76
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
77
                return
×
78
        }
79
        // compose the new user and send the verification code
80
        newUser := &db.User{
74✔
81
                ID:        userID,
74✔
82
                Email:     userInfo.Email,
74✔
83
                FirstName: userInfo.FirstName,
74✔
84
                LastName:  userInfo.LastName,
74✔
85
        }
74✔
86
        // generate a new verification code
74✔
87
        code, link, err := a.generateVerificationCodeAndLink(newUser, db.CodeTypeVerifyAccount)
74✔
88
        if err != nil {
74✔
89
                log.Warnw("could not generate verification code", "error", err)
×
90
                errors.ErrGenericInternalServerError.Write(w)
×
91
                return
×
92
        }
×
93
        // send the verification mail to the user email with the verification code
94
        // and the verification link
95
        if err := a.sendMail(r.Context(), userInfo.Email,
74✔
96
                mailtemplates.VerifyAccountNotification, struct {
74✔
97
                        Code string
74✔
98
                        Link string
74✔
99
                }{code, link},
74✔
100
        ); err != nil {
74✔
101
                log.Warnw("could not send verification code", "error", err)
×
102
                errors.ErrGenericInternalServerError.Write(w)
×
103
                return
×
104
        }
×
105
        // send the token back to the user
106
        apicommon.HTTPWriteOK(w)
74✔
107
}
108

109
// verifyUserAccountHandler godoc
110
//
111
//        @Summary                Verify user account
112
//        @Description        Verify a user account with the verification code
113
//        @Tags                        users
114
//        @Accept                        json
115
//        @Produce                json
116
//        @Param                        request        body                apicommon.UserVerification        true        "Verification information"
117
//        @Success                200                {object}        apicommon.LoginResponse
118
//        @Failure                400                {object}        errors.Error        "Invalid input data"
119
//        @Failure                401                {object}        errors.Error        "Unauthorized"
120
//        @Failure                409                {object}        errors.Error        "User already verified"
121
//        @Failure                410                {object}        errors.Error        "Verification code expired"
122
//        @Failure                500                {object}        errors.Error        "Internal server error"
123
//        @Router                        /users/verify [post]
124
func (a *API) verifyUserAccountHandler(w http.ResponseWriter, r *http.Request) {
67✔
125
        verification := &apicommon.UserVerification{}
67✔
126
        if err := json.NewDecoder(r.Body).Decode(verification); err != nil {
67✔
127
                errors.ErrMalformedBody.Write(w)
×
128
                return
×
129
        }
×
130
        // check the email and verification code are not empty only if the mail
131
        // service is available
132
        if a.mail != nil && (verification.Code == "" || verification.Email == "") {
67✔
133
                errors.ErrInvalidUserData.With("no verification code or email provided").Write(w)
×
134
                return
×
135
        }
×
136
        // get the user information from the database by email
137
        user, err := a.db.UserByEmail(verification.Email)
67✔
138
        if err != nil {
67✔
139
                if err == db.ErrNotFound {
×
140
                        errors.ErrUnauthorized.Write(w)
×
141
                        return
×
142
                }
×
143
                errors.ErrGenericInternalServerError.Write(w)
×
144
                return
×
145
        }
146
        // check the user is not already verified
147
        if user.Verified {
67✔
148
                errors.ErrUserAlreadyVerified.Write(w)
×
149
                return
×
150
        }
×
151
        // get the userVerification from the database
152
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
67✔
153
        if err != nil {
67✔
154
                if err != db.ErrNotFound {
×
155
                        log.Warnw("could not get verification code", "error", err)
×
156
                }
×
157
                errors.ErrUnauthorized.Write(w)
×
158
                return
×
159
        }
160
        // check the verification code is not expired
161
        if userVerification.Expiration.Before(time.Now()) {
67✔
162
                errors.ErrVerificationCodeExpired.Write(w)
×
163
                return
×
164
        }
×
165
        // check the verification code is correct
166
        code, err := internal.OpenToken(userVerification.SealedCode, verification.Email, a.secret)
67✔
167
        if err != nil {
67✔
168
                errors.ErrGenericInternalServerError.Write(w)
×
169
                return
×
170
        }
×
171
        if code != verification.Code {
67✔
172
                errors.ErrUnauthorized.With("code mismatch").Write(w)
×
173
                return
×
174
        }
×
175
        // verify the user account if the current verification code is valid and
176
        // matches with the provided one
177
        if err := a.db.VerifyUserAccount(user); err != nil {
67✔
178
                errors.ErrGenericInternalServerError.Write(w)
×
179
                return
×
180
        }
×
181
        // generate a new token with the user name as the subject
182
        res, err := a.buildLoginResponse(user.Email)
67✔
183
        if err != nil {
67✔
184
                errors.ErrGenericInternalServerError.Write(w)
×
185
                return
×
186
        }
×
187
        // send the token back to the user
188
        apicommon.HTTPWriteJSON(w, res)
67✔
189
}
190

191
// userVerificationCodeInfoHandler godoc
192
//
193
//        @Summary                Get verification code information
194
//        @Description        Get information about a user's verification code
195
//        @Tags                        users
196
//        @Accept                        json
197
//        @Produce                json
198
//        @Param                        email        query                string        true        "User email"
199
//        @Success                200                {object}        apicommon.UserVerification
200
//        @Failure                400                {object}        errors.Error        "Invalid input data"
201
//        @Failure                401                {object}        errors.Error        "Unauthorized"
202
//        @Failure                404                {object}        errors.Error        "User not found"
203
//        @Failure                409                {object}        errors.Error        "User already verified"
204
//        @Failure                500                {object}        errors.Error        "Internal server error"
205
//        @Router                        /users/verify/code [get]
206
func (a *API) userVerificationCodeInfoHandler(w http.ResponseWriter, r *http.Request) {
×
207
        // get the user email of the user from the request query
×
208
        userEmail := r.URL.Query().Get("email")
×
209
        // check the email is not empty
×
210
        if userEmail == "" {
×
211
                errors.ErrInvalidUserData.With("no email provided").Write(w)
×
212
                return
×
213
        }
×
214
        var err error
×
215
        var user *db.User
×
216
        // get the user information from the database by email
×
217
        user, err = a.db.UserByEmail(userEmail)
×
218
        if err != nil {
×
219
                if err == db.ErrNotFound {
×
220
                        errors.ErrUserNotFound.Write(w)
×
221
                        return
×
222
                }
×
223
                errors.ErrGenericInternalServerError.Write(w)
×
224
                return
×
225
        }
226
        // check if the user is already verified
227
        if user.Verified {
×
228
                errors.ErrUserAlreadyVerified.Write(w)
×
229
                return
×
230
        }
×
231
        // get the userVerification from the database
232
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
×
233
        if err != nil {
×
234
                if err != db.ErrNotFound {
×
235
                        log.Warnw("could not get verification code", "error", err)
×
236
                }
×
237
                errors.ErrUnauthorized.Write(w)
×
238
                return
×
239
        }
240
        // return the verification code information
241
        apicommon.HTTPWriteJSON(w, apicommon.UserVerification{
×
242
                Email:      user.Email,
×
243
                Expiration: userVerification.Expiration,
×
244
                Valid:      userVerification.Expiration.After(time.Now()),
×
245
        })
×
246
}
247

248
// resendUserVerificationCodeHandler godoc
249
//
250
//        @Summary                Resend verification code
251
//        @Description        Resend a verification code to the user's email
252
//        @Tags                        users
253
//        @Accept                        json
254
//        @Produce                json
255
//        @Param                        request        body                apicommon.UserVerification        true        "User email information"
256
//        @Success                200                {string}        string                                                "OK"
257
//        @Failure                400                {object}        errors.Error                                "Invalid input data, user already verified, or max resend attempts reached"
258
//        @Failure                401                {object}        errors.Error                                "Unauthorized"
259
//        @Failure                500                {object}        errors.Error                                "Internal server error"
260
//        @Router                        /users/verify/code [post]
261
func (a *API) resendUserVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
8✔
262
        verification := &apicommon.UserVerification{}
8✔
263
        if err := json.NewDecoder(r.Body).Decode(verification); err != nil {
9✔
264
                errors.ErrMalformedBody.Write(w)
1✔
265
                return
1✔
266
        }
1✔
267
        // check the email is not empty
268
        if verification.Email == "" {
8✔
269
                errors.ErrInvalidUserData.With("no email provided").Write(w)
1✔
270
                return
1✔
271
        }
1✔
272
        // get the user information from the database by email
273
        user, err := a.db.UserByEmail(verification.Email)
6✔
274
        if err != nil {
7✔
275
                if err == db.ErrNotFound {
2✔
276
                        errors.ErrUnauthorized.Write(w)
1✔
277
                        return
1✔
278
                }
1✔
279
                errors.ErrGenericInternalServerError.Write(w)
×
280
                return
×
281
        }
282
        // check the user is not already verified
283
        if user.Verified {
6✔
284
                errors.ErrUserAlreadyVerified.Write(w)
1✔
285
                return
1✔
286
        }
1✔
287
        // get the verification userVerification from the database
288
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
4✔
289
        if err != nil {
4✔
290
                if err != db.ErrNotFound {
×
291
                        log.Warnw("could not get verification code", "error", err)
×
292
                }
×
293
                errors.ErrUnauthorized.Write(w)
×
294
                return
×
295
        }
296
        // if the verification code is not expired
297
        if userVerification.Expiration.After(time.Now()) {
7✔
298
                // check if the maximum number of attempts has been reached for resending
3✔
299
                if userVerification.Attempts >= apicommon.VerificationCodeMaxAttempts {
4✔
300
                        errors.ErrVerificationMaxAttempts.WithData(apicommon.UserVerification{
1✔
301
                                Expiration: userVerification.Expiration,
1✔
302
                        }).Write(w)
1✔
303
                        return
1✔
304
                }
1✔
305
                code, err := internal.OpenToken(userVerification.SealedCode, user.Email, a.secret)
2✔
306
                if err != nil {
2✔
307
                        errors.ErrGenericInternalServerError.Write(w)
×
308
                        return
×
309
                }
×
310
                link, err := a.generateVerificationLink(user, code)
2✔
311
                if err != nil {
2✔
312
                        log.Warnw("could not generate verification link", "error", err)
×
313
                        errors.ErrGenericInternalServerError.Write(w)
×
314
                        return
×
315
                }
×
316
                // resend the existing verification code
317
                if err := a.sendMail(r.Context(), user.Email, mailtemplates.VerifyAccountNotification,
2✔
318
                        struct {
2✔
319
                                Code string
2✔
320
                                Link string
2✔
321
                        }{code, link},
2✔
322
                ); err != nil {
2✔
323
                        log.Warnw("could not resend verification code", "error", err)
×
324
                        errors.ErrGenericInternalServerError.Write(w)
×
325
                        return
×
326
                }
×
327
                if err = a.db.VerificationCodeIncrementAttempts(userVerification.SealedCode, db.CodeTypeVerifyAccount); err != nil {
2✔
328
                        log.Warnw("could not increment verification code attempts", "error", err)
×
329
                        errors.ErrGenericInternalServerError.Write(w)
×
330
                        return
×
331
                }
×
332
                // return the verification code information
333
                apicommon.HTTPWriteJSON(w, apicommon.UserVerification{
2✔
334
                        Expiration: userVerification.Expiration,
2✔
335
                })
2✔
336
                return
2✔
337
        }
338

339
        // generate a new verification code
340
        newCode, link, err := a.generateVerificationCodeAndLink(user, db.CodeTypeVerifyAccount)
1✔
341
        if err != nil {
1✔
342
                log.Warnw("could not generate verification code", "error", err)
×
343
                errors.ErrGenericInternalServerError.Write(w)
×
344
                return
×
345
        }
×
346
        // send the verification mail to the user email with the verification code
347
        // and the verification link
348
        if err := a.sendMail(r.Context(), user.Email, mailtemplates.VerifyAccountNotification,
1✔
349
                struct {
1✔
350
                        Code string
1✔
351
                        Link string
1✔
352
                }{newCode, link},
1✔
353
        ); err != nil {
1✔
354
                log.Warnw("could not send verification code", "error", err)
×
355
                errors.ErrGenericInternalServerError.Write(w)
×
356
                return
×
357
        }
×
358
        apicommon.HTTPWriteOK(w)
1✔
359
}
360

361
// userInfoHandler godoc
362
//
363
//        @Summary                Get user information
364
//        @Description        Get information about the authenticated user
365
//        @Tags                        users
366
//        @Accept                        json
367
//        @Produce                json
368
//        @Security                BearerAuth
369
//        @Success                200        {object}        apicommon.UserInfo
370
//        @Failure                401        {object}        errors.Error        "Unauthorized"
371
//        @Failure                500        {object}        errors.Error        "Internal server error"
372
//        @Router                        /users/me [get]
373
func (a *API) userInfoHandler(w http.ResponseWriter, r *http.Request) {
78✔
374
        user, ok := apicommon.UserFromContext(r.Context())
78✔
375
        if !ok {
78✔
376
                errors.ErrUnauthorized.Write(w)
×
377
                return
×
378
        }
×
379
        // get the user organizations information from the database if any
380
        userOrgs := make([]*apicommon.UserOrganization, 0)
78✔
381
        for _, orgInfo := range user.Organizations {
82✔
382
                org, parent, err := a.db.OrganizationWithParent(orgInfo.Address)
4✔
383
                if err != nil {
4✔
384
                        if err == db.ErrNotFound {
×
385
                                continue
×
386
                        }
387
                        errors.ErrGenericInternalServerError.Write(w)
×
388
                        return
×
389
                }
390
                userOrgs = append(userOrgs, &apicommon.UserOrganization{
4✔
391
                        Role:         string(orgInfo.Role),
4✔
392
                        Organization: apicommon.OrganizationFromDB(org, parent),
4✔
393
                })
4✔
394
        }
395
        // extract the list of linked OAuth providers
396
        providers := make([]string, 0, len(user.OAuth))
78✔
397
        for provider := range user.OAuth {
78✔
NEW
398
                providers = append(providers, provider)
×
NEW
399
        }
×
400
        // return the user information
401
        apicommon.HTTPWriteJSON(w, apicommon.UserInfo{
78✔
402
                ID:            user.ID,
78✔
403
                Email:         user.Email,
78✔
404
                FirstName:     user.FirstName,
78✔
405
                LastName:      user.LastName,
78✔
406
                Verified:      user.Verified,
78✔
407
                HasPassword:   user.Password != "",
78✔
408
                Providers:     providers,
78✔
409
                Organizations: userOrgs,
78✔
410
        })
78✔
411
}
412

413
// updateUserInfoHandler godoc
414
//
415
//        @Summary                Update user information
416
//        @Description        Update information for the authenticated user
417
//        @Tags                        users
418
//        @Accept                        json
419
//        @Produce                json
420
//        @Security                BearerAuth
421
//        @Param                        request        body                apicommon.UserInfo        true        "User information to update"
422
//        @Success                200                {object}        apicommon.LoginResponse
423
//        @Failure                400                {object}        errors.Error        "Invalid input data"
424
//        @Failure                401                {object}        errors.Error        "Unauthorized"
425
//        @Failure                500                {object}        errors.Error        "Internal server error"
426
//        @Router                        /users/me [put]
427
func (a *API) updateUserInfoHandler(w http.ResponseWriter, r *http.Request) {
×
428
        user, ok := apicommon.UserFromContext(r.Context())
×
429
        if !ok {
×
430
                errors.ErrUnauthorized.Write(w)
×
431
                return
×
432
        }
×
433
        userInfo := &apicommon.UserInfo{}
×
434
        if err := json.NewDecoder(r.Body).Decode(userInfo); err != nil {
×
435
                errors.ErrMalformedBody.Write(w)
×
436
                return
×
437
        }
×
438
        // create a flag to check if the user information has changed and needs to
439
        // be updated and store the current email to check if it has changed
440
        // specifically
441
        updateUser := false
×
442
        currentEmail := user.Email
×
443
        // check the email is correct format if it is not empty
×
444
        if userInfo.Email != "" {
×
445
                if !internal.ValidEmail(userInfo.Email) {
×
446
                        errors.ErrEmailMalformed.Write(w)
×
447
                        return
×
448
                }
×
449
                // update the user email and set the flag to true to update the user
450
                // info
451
                user.Email = userInfo.Email
×
452
                updateUser = true
×
453
        }
454
        // check the first name is not empty
455
        if userInfo.FirstName != "" {
×
456
                // update the user first name and set the flag to true to update the
×
457
                // user info
×
458
                user.FirstName = userInfo.FirstName
×
459
                updateUser = true
×
460
        }
×
461
        // check the last name is not empty
462
        if userInfo.LastName != "" {
×
463
                // update the user last name and set the flag to true to update the
×
464
                // user info
×
465
                user.LastName = userInfo.LastName
×
466
                updateUser = true
×
467
        }
×
468
        // update the user information if needed
469
        if updateUser {
×
470
                if _, err := a.db.SetUser(user); err != nil {
×
471
                        log.Warnw("could not update user", "error", err)
×
472
                        errors.ErrGenericInternalServerError.Write(w)
×
473
                        return
×
474
                }
×
475
                // if user email has changed, update the creator email in the
476
                // organizations where the user is creator
477
                if user.Email != currentEmail {
×
478
                        if err := a.db.ReplaceCreatorEmail(currentEmail, user.Email); err != nil {
×
479
                                // revert the user update if the creator email update fails
×
480
                                user.Email = currentEmail
×
481
                                if _, err := a.db.SetUser(user); err != nil {
×
482
                                        log.Warnw("could not revert user update", "error", err)
×
483
                                }
×
484
                                // return an error
485
                                errors.ErrGenericInternalServerError.Write(w)
×
486
                                return
×
487
                        }
488
                }
489
        }
490
        // generate a new token with the new user email as the subject
491
        res, err := a.buildLoginResponse(user.Email)
×
492
        if err != nil {
×
493
                errors.ErrGenericInternalServerError.Write(w)
×
494
                return
×
495
        }
×
496
        apicommon.HTTPWriteJSON(w, res)
×
497
}
498

499
// updateUserPasswordHandler godoc
500
//
501
//        @Summary                Update user password
502
//        @Description        Update the password for the authenticated user
503
//        @Tags                        users
504
//        @Accept                        json
505
//        @Produce                json
506
//        @Security                BearerAuth
507
//        @Param                        request        body                apicommon.UserPasswordUpdate        true        "Password update information"
508
//        @Success                200                {string}        string                                                        "OK"
509
//        @Failure                400                {object}        errors.Error                                        "Invalid input data"
510
//        @Failure                401                {object}        errors.Error                                        "Unauthorized or old password does not match"
511
//        @Failure                500                {object}        errors.Error                                        "Internal server error"
512
//        @Router                        /users/password [put]
513
func (a *API) updateUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
×
514
        user, ok := apicommon.UserFromContext(r.Context())
×
515
        if !ok {
×
516
                errors.ErrUnauthorized.Write(w)
×
517
                return
×
518
        }
×
519
        // check if user is OAuth-only (no password set)
NEW
520
        if user.Password == "" {
×
NEW
521
                errors.ErrOAuthUserCannotUsePasswordRecovery.Write(w)
×
NEW
522
                return
×
NEW
523
        }
×
524
        userPasswords := &apicommon.UserPasswordUpdate{}
×
525
        if err := json.NewDecoder(r.Body).Decode(userPasswords); err != nil {
×
526
                errors.ErrMalformedBody.Write(w)
×
527
                return
×
528
        }
×
529
        // check the password is correct format
530
        if len(userPasswords.NewPassword) < 8 {
×
531
                errors.ErrPasswordTooShort.Write(w)
×
532
                return
×
533
        }
×
534
        // hash the password the old password to compare it with the stored one
535
        hOldPassword := internal.HexHashPassword(passwordSalt, userPasswords.OldPassword)
×
536
        if hOldPassword != user.Password {
×
537
                errors.ErrUnauthorized.Withf("old password does not match").Write(w)
×
538
                return
×
539
        }
×
540
        // hash and update the new password
541
        user.Password = internal.HexHashPassword(passwordSalt, userPasswords.NewPassword)
×
542
        if _, err := a.db.SetUser(user); err != nil {
×
543
                log.Warnw("could not update user password", "error", err)
×
544
                errors.ErrGenericInternalServerError.Write(w)
×
545
                return
×
546
        }
×
547
        apicommon.HTTPWriteOK(w)
×
548
}
549

550
// recoverUserPasswordHandler godoc
551
//
552
//        @Summary                Recover user password
553
//        @Description        Request a password recovery code for a user
554
//        @Tags                        users
555
//        @Accept                        json
556
//        @Produce                json
557
//        @Param                        request        body                apicommon.UserInfo        true        "User email information"
558
//        @Success                200                {string}        string                                "OK"
559
//        @Failure                400                {object}        errors.Error                "Invalid input data"
560
//        @Failure                500                {object}        errors.Error                "Internal server error"
561
//        @Router                        /users/recovery [post]
562
func (a *API) recoverUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
1✔
563
        // get the user info from the request body
1✔
564
        userInfo := &apicommon.UserInfo{}
1✔
565
        if err := json.NewDecoder(r.Body).Decode(userInfo); err != nil {
1✔
566
                errors.ErrMalformedBody.Write(w)
×
567
                return
×
568
        }
×
569
        // get the user information from the database by email
570
        user, err := a.db.UserByEmail(userInfo.Email)
1✔
571
        if err != nil {
1✔
572
                if err == db.ErrNotFound {
×
573
                        // do not return an error if the user is not found to avoid
×
574
                        // information leakage
×
575
                        apicommon.HTTPWriteOK(w)
×
576
                        return
×
577
                }
×
578
                errors.ErrGenericInternalServerError.Write(w)
×
579
                return
×
580
        }
581
        // check the user is verified
582
        if user.Verified {
2✔
583
                // generate a new verification code
1✔
584
                code, link, err := a.generateVerificationCodeAndLink(user, db.CodeTypePasswordReset)
1✔
585
                if err != nil {
1✔
586
                        log.Warnw("could not generate verification code", "error", err)
×
587
                        errors.ErrGenericInternalServerError.Write(w)
×
588
                        return
×
589
                }
×
590
                // send the password reset mail to the user email with the verification
591
                // code and the verification link
592
                if err := a.sendMail(r.Context(), user.Email, mailtemplates.PasswordResetNotification,
1✔
593
                        struct {
1✔
594
                                Code string
1✔
595
                                Link string
1✔
596
                        }{code, link},
1✔
597
                ); err != nil {
1✔
598
                        log.Warnw("could not send reset passworod code", "error", err)
×
599
                        errors.ErrGenericInternalServerError.Write(w)
×
600
                        return
×
601
                }
×
602
        }
603
        apicommon.HTTPWriteOK(w)
1✔
604
}
605

606
// resetUserPasswordHandler godoc
607
//
608
//        @Summary                Reset user password
609
//        @Description        Reset a user's password using a verification code
610
//        @Tags                        users
611
//        @Accept                        json
612
//        @Produce                json
613
//        @Param                        request        body                apicommon.UserPasswordReset        true        "Password reset information"
614
//        @Success                200                {string}        string                                                "OK"
615
//        @Failure                400                {object}        errors.Error                                "Invalid input data"
616
//        @Failure                401                {object}        errors.Error                                "Unauthorized or invalid verification code"
617
//        @Failure                500                {object}        errors.Error                                "Internal server error"
618
//        @Router                        /users/reset [post]
619
func (a *API) resetUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
1✔
620
        userPasswords := &apicommon.UserPasswordReset{}
1✔
621
        if err := json.NewDecoder(r.Body).Decode(userPasswords); err != nil {
1✔
622
                errors.ErrMalformedBody.Write(w)
×
623
                return
×
624
        }
×
625
        // check the password is correct format
626
        if len(userPasswords.NewPassword) < 8 {
1✔
627
                errors.ErrPasswordTooShort.Write(w)
×
628
                return
×
629
        }
×
630

631
        // get the user information from the database by email
632
        user, err := a.db.UserByEmail(userPasswords.Email)
1✔
633
        if err != nil {
1✔
634
                if err == db.ErrNotFound {
×
635
                        errors.ErrUserNotFound.Write(w)
×
636
                        return
×
637
                }
×
638
                errors.ErrGenericInternalServerError.Write(w)
×
639
                return
×
640
        }
641

642
        // check if user is OAuth-only (no password set)
643
        if user.Password == "" {
1✔
NEW
644
                errors.ErrOAuthUserCannotUsePasswordRecovery.Write(w)
×
NEW
645
                return
×
NEW
646
        }
×
647

648
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypePasswordReset)
1✔
649
        if err != nil {
1✔
650
                if err != db.ErrNotFound {
×
651
                        log.Warnw("could not get verification code", "error", err)
×
652
                }
×
653
                errors.ErrUnauthorized.Write(w)
×
654
                return
×
655
        }
656

657
        // check the verification code is not expired
658
        if userVerification.Expiration.Before(time.Now()) {
1✔
659
                errors.ErrVerificationCodeExpired.Write(w)
×
660
                return
×
661
        }
×
662

663
        code, err := internal.OpenToken(userVerification.SealedCode, userPasswords.Email, a.secret)
1✔
664
        if err != nil {
1✔
665
                errors.ErrGenericInternalServerError.Write(w)
×
666
                return
×
667
        }
×
668
        if code != userPasswords.Code {
1✔
669
                errors.ErrUnauthorized.With("code mismatch").Write(w)
×
670
        }
×
671

672
        // hash and update the new password
673
        user.Password = internal.HexHashPassword(passwordSalt, userPasswords.NewPassword)
1✔
674
        if _, err := a.db.SetUser(user); err != nil {
1✔
675
                log.Warnw("could not update user password", "error", err)
×
676
                errors.ErrGenericInternalServerError.Write(w)
×
677
                return
×
678
        }
×
679
        apicommon.HTTPWriteOK(w)
1✔
680
}
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