|
|
@@ -65,7 +65,7 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
|
|
- if err := app.sendUser(w, user.ID, user.Email, redirect); err != nil {
|
|
|
+ if err := app.sendUser(w, user.ID, user.Email, false, redirect); err != nil {
|
|
|
app.handleErrorFormDecoding(err, ErrUserDecode, w)
|
|
|
return
|
|
|
}
|
|
|
@@ -86,7 +86,7 @@ func (app *App) HandleAuthCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if err := app.sendUser(w, tok.IBy, user.Email, ""); err != nil {
|
|
|
+ if err := app.sendUser(w, tok.IBy, user.Email, user.EmailVerified, ""); err != nil {
|
|
|
app.handleErrorFormDecoding(err, ErrUserDecode, w)
|
|
|
return
|
|
|
}
|
|
|
@@ -103,9 +103,19 @@ func (app *App) HandleAuthCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
userID, _ := session.Values["user_id"].(uint)
|
|
|
email, _ := session.Values["email"].(string)
|
|
|
+
|
|
|
+ user, err := app.Repo.User.ReadUser(userID)
|
|
|
+
|
|
|
+ fmt.Println("EMAIL VERIFIED IS", user.EmailVerified)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
|
- if err := app.sendUser(w, userID, email, ""); err != nil {
|
|
|
+ if err := app.sendUser(w, userID, email, user.EmailVerified, ""); err != nil {
|
|
|
app.handleErrorFormDecoding(err, ErrUserDecode, w)
|
|
|
return
|
|
|
}
|
|
|
@@ -262,7 +272,7 @@ func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
|
- if err := app.sendUser(w, storedUser.ID, storedUser.Email, redirect); err != nil {
|
|
|
+ if err := app.sendUser(w, storedUser.ID, storedUser.Email, storedUser.EmailVerified, redirect); err != nil {
|
|
|
app.handleErrorFormDecoding(err, ErrUserDecode, w)
|
|
|
return
|
|
|
}
|
|
|
@@ -354,6 +364,164 @@ func (app *App) HandleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// InitiateEmailVerifyUser initiates the email verification flow for a logged-in user
|
|
|
+func (app *App) InitiateEmailVerifyUser(w http.ResponseWriter, r *http.Request) {
|
|
|
+ userID, err := app.getUserIDFromRequest(r)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ app.handleErrorInternal(err, w)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ user, err := app.Repo.User.ReadUser(userID)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ app.handleErrorInternal(err, w)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // error already handled by helper
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ form := &forms.InitiateResetUserPasswordForm{
|
|
|
+ Email: user.Email,
|
|
|
+ }
|
|
|
+
|
|
|
+ // convert the form to a pw reset token model
|
|
|
+ pwReset, rawToken, err := form.ToPWResetToken()
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ app.handleErrorFormDecoding(err, ErrProjectDecode, w)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // handle write to the database
|
|
|
+ pwReset, err = app.Repo.PWResetToken.CreatePWResetToken(pwReset)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ app.handleErrorDataWrite(err, w)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ queryVals := url.Values{
|
|
|
+ "token": []string{rawToken},
|
|
|
+ "token_id": []string{fmt.Sprintf("%d", pwReset.ID)},
|
|
|
+ }
|
|
|
+
|
|
|
+ sgClient := email.SendgridClient{
|
|
|
+ APIKey: app.ServerConf.SendgridAPIKey,
|
|
|
+ VerifyEmailTemplateID: app.ServerConf.SendgridVerifyEmailTemplateID,
|
|
|
+ SenderEmail: app.ServerConf.SendgridSenderEmail,
|
|
|
+ }
|
|
|
+
|
|
|
+ err = sgClient.SendEmailVerification(
|
|
|
+ fmt.Sprintf("%s/api/email/verify/finalize?%s", app.ServerConf.ServerURL, queryVals.Encode()),
|
|
|
+ form.Email,
|
|
|
+ )
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ app.handleErrorInternal(err, w)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// FinalizEmailVerifyUser completes the email verification flow for a user.
|
|
|
+func (app *App) FinalizEmailVerifyUser(w http.ResponseWriter, r *http.Request) {
|
|
|
+ userID, err := app.getUserIDFromRequest(r)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ app.handleErrorInternal(err, w)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ user, err := app.Repo.User.ReadUser(userID)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ app.handleErrorInternal(err, w)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ vals, err := url.ParseQuery(r.URL.RawQuery)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Invalid email verification URL"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var tokenStr string
|
|
|
+ var tokenID uint
|
|
|
+
|
|
|
+ if tokenArr, ok := vals["token"]; ok && len(tokenArr) == 1 {
|
|
|
+ tokenStr = tokenArr[0]
|
|
|
+ } else {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Invalid email verification URL: token required"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if tokenIDArr, ok := vals["token_id"]; ok && len(tokenIDArr) == 1 {
|
|
|
+ id, err := strconv.ParseUint(tokenIDArr[0], 10, 64)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Invalid email verification URL: valid token id required"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ tokenID = uint(id)
|
|
|
+ } else {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Invalid email verification URL: valid token id required"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // verify the token is valid
|
|
|
+ token, err := app.Repo.PWResetToken.ReadPWResetToken(tokenID)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Email verification error: valid token required"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // make sure the token is still valid and has not expired
|
|
|
+ if !token.IsValid || token.IsExpired() {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Email verification error: valid token required"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // make sure the token is correct
|
|
|
+ if err := bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(tokenStr)); err != nil {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Email verification error: valid token required"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ user.EmailVerified = true
|
|
|
+
|
|
|
+ user, err = app.Repo.User.UpdateUser(user)
|
|
|
+
|
|
|
+ fmt.Println("UPDATED USER WITH VERIFIED EMAIL", user)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Could not verify email address"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // invalidate the token
|
|
|
+ token.IsValid = false
|
|
|
+
|
|
|
+ _, err = app.Repo.PWResetToken.UpdatePWResetToken(token)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ http.Redirect(w, r, "/dashboard?error="+url.QueryEscape("Could not verify email address"), 302)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ http.Redirect(w, r, "/dashboard", 302)
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
// InitiatePWResetUser initiates the password reset flow based on an email. The endpoint
|
|
|
// checks if the email exists, but returns a 200 status code regardless, since we don't
|
|
|
// want to leak in-use emails
|
|
|
@@ -660,16 +828,18 @@ func doesUserExist(repo *repository.Repository, user *models.User) *HTTPError {
|
|
|
}
|
|
|
|
|
|
type SendUserExt struct {
|
|
|
- ID uint `json:"id"`
|
|
|
- Email string `json:"email"`
|
|
|
- Redirect string `json:"redirect,omitempty"`
|
|
|
+ ID uint `json:"id"`
|
|
|
+ Email string `json:"email"`
|
|
|
+ EmailVerified bool `json:"email_verified"`
|
|
|
+ Redirect string `json:"redirect,omitempty"`
|
|
|
}
|
|
|
|
|
|
-func (app *App) sendUser(w http.ResponseWriter, userID uint, email, redirect string) error {
|
|
|
+func (app *App) sendUser(w http.ResponseWriter, userID uint, email string, emailVerified bool, redirect string) error {
|
|
|
resUser := &SendUserExt{
|
|
|
- ID: userID,
|
|
|
- Email: email,
|
|
|
- Redirect: redirect,
|
|
|
+ ID: userID,
|
|
|
+ Email: email,
|
|
|
+ EmailVerified: emailVerified,
|
|
|
+ Redirect: redirect,
|
|
|
}
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(resUser); err != nil {
|