Skip to content

Commit

Permalink
generateVerificationCode reimplemented to a generic method to generat…
Browse files Browse the repository at this point in the history
…e a verification code and link for any kind of database code type, org invite verification code included
  • Loading branch information
lucasmenendez committed Nov 22, 2024
1 parent b9d41d0 commit 3a7ebdb
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 93 deletions.
73 changes: 73 additions & 0 deletions api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import (
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/internal"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/dvote/util"
)

// organizationFromRequest helper function allows to get the organization info
Expand Down Expand Up @@ -73,6 +76,76 @@ func (a *API) buildWebAppURL(path string, params map[string]any) (string, error)
return url.String(), nil
}

// generateVerificationCodeAndLink method generates and stores in the database
// a new verification code for the target provided according to the database
// code type selected. Then it generates a verification link to the web app
// with correct web app uri and link parameters for the code type selected.
// It returns the generated verification code, the link to the web app and
// an error if the verification code could not be generated or stored in the
// database.
func (a *API) generateVerificationCodeAndLink(target any, codeType db.CodeType) (string, string, error) {
// generate verification code if the mail service is available, if not
// the verification code will not be sent but stored in the database
// generated with just the user email to mock the verification process
var code string
if a.mail != nil {
code = util.RandomHex(VerificationCodeLength)
}
var webAppURI string
var linkParams map[string]any
switch codeType {
case db.CodeTypeVerifyAccount, db.CodeTypePasswordReset:
// the target should be a database user
user, ok := target.(*db.User)
if !ok {
return "", "", fmt.Errorf("invalid target type")
}
// generate the verification code for the user and the expiration time
hashCode := internal.HashVerificationCode(user.Email, code)
exp := time.Now().Add(VerificationCodeExpiration)
// store the verification code in the database
if err := a.db.SetVerificationCode(&db.User{ID: user.ID}, hashCode, codeType, exp); err != nil {
return "", "", err
}
// set the web app URI and the link parameters
webAppURI = mailtemplates.VerifyAccountNotification.WebAppURI
if codeType == db.CodeTypePasswordReset {
webAppURI = mailtemplates.PasswordResetNotification.WebAppURI
}
linkParams = map[string]any{
"email": user.Email,
"code": code,
}
case db.CodeTypeOrgInvite:
// the target should be a database organization invite
invite, ok := target.(*db.OrganizationInvite)
if !ok {
return "", "", fmt.Errorf("invalid target type")
}
// set the verification code for the organization invite and the
// expiration time
invite.InvitationCode = code
invite.Expiration = time.Now().Add(InvitationExpiration)
// store the organization invite in the database
if err := a.db.CreateInvitation(invite); err != nil {
return "", "", err
}
// set the web app URI and the link parameters
webAppURI = mailtemplates.InviteNotification.WebAppURI
linkParams = map[string]any{
"email": invite.NewUserEmail,
"code": invite.InvitationCode,
"address": invite.OrganizationAddress,
}
default:
return "", "", fmt.Errorf("invalid code type")
}
// generate the verification link to the web app with the selected uri
// and the link parameters
verificationLink, err := a.buildWebAppURL(webAppURI, linkParams)
return code, verificationLink, err
}

// httpWriteJSON helper function allows to write a JSON response.
func httpWriteJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
Expand Down
26 changes: 7 additions & 19 deletions api/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,32 +281,20 @@ func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Req
return
}
// create new invitation
inviteCode := internal.RandomHex(VerificationCodeLength)
if err := a.db.CreateInvitation(&db.OrganizationInvite{
InvitationCode: inviteCode,
orgInvite := &db.OrganizationInvite{
OrganizationAddress: org.Address,
NewUserEmail: invite.Email,
Role: db.UserRole(invite.Role),
CurrentUserID: user.ID,
Expiration: time.Now().Add(InvitationExpiration),
}); err != nil {
}
// generate the verification code and the verification link
code, link, err := a.generateVerificationCodeAndLink(orgInvite, db.CodeTypeOrgInvite)
if err != nil {
if err == db.ErrAlreadyExists {
ErrDuplicateConflict.With("user is already invited to the organization").Write(w)
return
}
ErrGenericInternalServerError.Withf("could not create invitation: %v", err).Write(w)
return
}
// send the invitation verification code to the user email
inviteLink, err := a.buildWebAppURL(mailtemplates.InviteNotification.WebAppURI,
map[string]any{
"email": invite.Email,
"code": inviteCode,
"address": org.Address,
})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
ErrGenericInternalServerError.Withf("could not create the invite: %v", err).Write(w)
return
}
// send the invitation mail to invited user email with the invite code and
Expand All @@ -316,7 +304,7 @@ func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Req
Organization string
Code string
Link string
}{org.Address, inviteCode, inviteLink},
}{org.Address, code, link},
); err != nil {
log.Warnw("could not send verification code email", "error", err)
ErrGenericInternalServerError.Write(w)
Expand Down
70 changes: 9 additions & 61 deletions api/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,8 @@ import (
"github.com/vocdoni/saas-backend/internal"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/dvote/util"
)

// generateVerificationCode method generates and stores in the database a new
// verification code for the user and verification code type provided. Both
// parameters are required. It returns the generated verification code and an
// error if the verification code could not be generated or stored in the database.
func (a *API) generateVerificationCode(user *db.User, codeType db.CodeType) (string, error) {
// generate verification code if the mail service is available, if not
// the verification code will not be sent but stored in the database
// generated with just the user email to mock the verification process
var code string
if a.mail != nil {
code = util.RandomHex(VerificationCodeLength)
}
// store the verification code in the database
hashCode := internal.HashVerificationCode(user.Email, code)
exp := time.Now().Add(VerificationCodeExpiration)
if err := a.db.SetVerificationCode(&db.User{ID: user.ID}, hashCode, codeType, exp); err != nil {
return "", err
}
return code, nil
}

// registerHandler handles the register request. It creates a new user in the database.
func (a *API) registerHandler(w http.ResponseWriter, r *http.Request) {
userInfo := &UserInfo{}
Expand Down Expand Up @@ -92,27 +70,19 @@ func (a *API) registerHandler(w http.ResponseWriter, r *http.Request) {
LastName: userInfo.LastName,
}
// generate a new verification code
code, err := a.generateVerificationCode(newUser, db.CodeTypeAccountVerification)
code, link, err := a.generateVerificationCodeAndLink(newUser, db.CodeTypeVerifyAccount)
if err != nil {
log.Warnw("could not generate verification code", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
// send the new verification code to the user email
verificationLink, err := a.buildWebAppURL(mailtemplates.VerifyAccountNotification.WebAppURI,
map[string]any{"email": newUser.Email, "code": code})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
// send the verification mail to the user email with the verification code
// and the verification link
if err := a.sendMail(r.Context(), userInfo.Email,
mailtemplates.VerifyAccountNotification, struct {
Code string
Link string
}{code, verificationLink},
}{code, link},
); err != nil {
log.Warnw("could not send verification code", "error", err)
ErrGenericInternalServerError.Write(w)
Expand Down Expand Up @@ -159,7 +129,7 @@ func (a *API) verifyUserAccountHandler(w http.ResponseWriter, r *http.Request) {
return
}
// get the verification code from the database
code, err := a.db.UserVerificationCode(user, db.CodeTypeAccountVerification)
code, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
if err != nil {
if err != db.ErrNotFound {
log.Warnw("could not get verification code", "error", err)
Expand Down Expand Up @@ -228,7 +198,7 @@ func (a *API) userVerificationCodeInfoHandler(w http.ResponseWriter, r *http.Req
return
}
// get the verification code from the database
code, err := a.db.UserVerificationCode(user, db.CodeTypeAccountVerification)
code, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
if err != nil {
if err != db.ErrNotFound {
log.Warnw("could not get verification code", "error", err)
Expand Down Expand Up @@ -277,7 +247,7 @@ func (a *API) resendUserVerificationCodeHandler(w http.ResponseWriter, r *http.R
return
}
// get the verification code from the database
code, err := a.db.UserVerificationCode(user, db.CodeTypeAccountVerification)
code, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
if err != nil {
if err != db.ErrNotFound {
log.Warnw("could not get verification code", "error", err)
Expand All @@ -291,30 +261,19 @@ func (a *API) resendUserVerificationCodeHandler(w http.ResponseWriter, r *http.R
return
}
// generate a new verification code
newCode, err := a.generateVerificationCode(user, db.CodeTypeAccountVerification)
newCode, link, err := a.generateVerificationCodeAndLink(user, db.CodeTypeVerifyAccount)
if err != nil {
log.Warnw("could not generate verification code", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
// send the new verification code to the user email
verificationLink, err := a.buildWebAppURL(mailtemplates.VerifyAccountNotification.WebAppURI,
map[string]any{
"email": user.Email,
"code": newCode,
})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
// send the verification mail to the user email with the verification code
// and the verification link
if err := a.sendMail(r.Context(), user.Email, mailtemplates.VerifyAccountNotification,
struct {
Code string
Link string
}{newCode, verificationLink},
}{newCode, link},
); err != nil {
log.Warnw("could not send verification code", "error", err)
ErrGenericInternalServerError.Write(w)
Expand Down Expand Up @@ -492,30 +451,19 @@ func (a *API) recoverUserPasswordHandler(w http.ResponseWriter, r *http.Request)
// check the user is verified
if user.Verified {
// generate a new verification code
code, err := a.generateVerificationCode(user, db.CodeTypePasswordReset)
code, link, err := a.generateVerificationCodeAndLink(user, db.CodeTypePasswordReset)
if err != nil {
log.Warnw("could not generate verification code", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
// send the password reset code to the user email
resetLink, err := a.buildWebAppURL(mailtemplates.PasswordResetNotification.WebAppURI,
map[string]any{
"email": user.Email,
"code": code,
})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
// send the password reset mail to the user email with the verification
// code and the verification link
if err := a.sendMail(r.Context(), user.Email, mailtemplates.PasswordResetNotification,
struct {
Code string
Link string
}{code, resetLink},
}{code, link},
); err != nil {
log.Warnw("could not send reset passworod code", "error", err)
ErrGenericInternalServerError.Write(w)
Expand Down
4 changes: 3 additions & 1 deletion api/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ func init() {
// create a regex to find the verification code in the email
codeRgx := fmt.Sprintf(`(.{%d})`, VerificationCodeLength*2)
// load the email templates
mailtemplates.Load("../assets")
if err := mailtemplates.Load("../assets"); err != nil {
panic(err)
}
// wrap the mail template execution to force plain body and set the regex
// needle as the verification code
testTemplateExec := func(mt mailtemplates.MailTemplate) (*notifications.Notification, error) {
Expand Down
5 changes: 3 additions & 2 deletions db/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ const (
ProfessionalCollegeType OrganizationType = "professional_college"
OthersType OrganizationType = "others"
// verification code types
CodeTypeAccountVerification CodeType = "account"
CodeTypePasswordReset CodeType = "password"
CodeTypeVerifyAccount CodeType = "verify_account"
CodeTypePasswordReset CodeType = "password_reset"
CodeTypeOrgInvite CodeType = "organization_invite"
)

// writableRoles is a map that contains if the role is writable or not
Expand Down
2 changes: 1 addition & 1 deletion db/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (ms *MongoStorage) VerifyUserAccount(user *User) error {
return err
}
// remove the verification code
return ms.delVerificationCode(ctx, user.ID, CodeTypeAccountVerification)
return ms.delVerificationCode(ctx, user.ID, CodeTypeVerifyAccount)
}

// IsMemberOf method checks if the user with the given email is a member of the
Expand Down
18 changes: 9 additions & 9 deletions db/verifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ func TestUserVerificationCode(t *testing.T) {
})
c.Assert(err, qt.IsNil)

_, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification)
_, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeVerifyAccount)
c.Assert(err, qt.Equals, ErrNotFound)

testCode := "testCode"
c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification, time.Now()), qt.IsNil)
c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeVerifyAccount, time.Now()), qt.IsNil)

code, err := db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification)
code, err := db.UserVerificationCode(&User{ID: userID}, CodeTypeVerifyAccount)
c.Assert(err, qt.IsNil)
c.Assert(code.Code, qt.Equals, testCode)

c.Assert(db.VerifyUserAccount(&User{ID: userID}), qt.IsNil)
_, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification)
_, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeVerifyAccount)
c.Assert(err, qt.Equals, ErrNotFound)
}

Expand All @@ -47,7 +47,7 @@ func TestSetVerificationCode(t *testing.T) {
}()

nonExistingUserID := uint64(100)
err := db.SetVerificationCode(&User{ID: nonExistingUserID}, "testCode", CodeTypeAccountVerification, time.Now())
err := db.SetVerificationCode(&User{ID: nonExistingUserID}, "testCode", CodeTypeVerifyAccount, time.Now())
c.Assert(err, qt.Equals, ErrNotFound)

userID, err := db.SetUser(&User{
Expand All @@ -59,16 +59,16 @@ func TestSetVerificationCode(t *testing.T) {
c.Assert(err, qt.IsNil)

testCode := "testCode"
c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification, time.Now()), qt.IsNil)
c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeVerifyAccount, time.Now()), qt.IsNil)

code, err := db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification)
code, err := db.UserVerificationCode(&User{ID: userID}, CodeTypeVerifyAccount)
c.Assert(err, qt.IsNil)
c.Assert(code.Code, qt.Equals, testCode)

testCode = "testCode2"
c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification, time.Now()), qt.IsNil)
c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeVerifyAccount, time.Now()), qt.IsNil)

code, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification)
code, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeVerifyAccount)
c.Assert(err, qt.IsNil)
c.Assert(code.Code, qt.Equals, testCode)
}

0 comments on commit 3a7ebdb

Please sign in to comment.