/
forgot_password_handler.go
147 lines (125 loc) · 4.88 KB
/
forgot_password_handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package httphandler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant"
"github.com/stellar/go/support/log"
"github.com/stellar/go/support/render/httpjson"
"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/internal/htmltemplate"
"github.com/stellar/stellar-disbursement-platform-backend/internal/message"
"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror"
"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/auth"
)
const forgotPasswordMessageTitle = "Reset Account Password"
// ForgotPasswordHandler searches for the user that is requesting a password reset
// and sends an email with a link to access the password reset page.
type ForgotPasswordHandler struct {
AuthManager auth.AuthManager
MessengerClient message.MessengerClient
Models *data.Models
ReCAPTCHAValidator validators.ReCAPTCHAValidator
ReCAPTCHADisabled bool
}
type ForgotPasswordRequest struct {
Email string `json:"email"`
ReCAPTCHAToken string `json:"recaptcha_token"`
}
type ForgotPasswordResponseBody struct {
Message string `json:"message"`
}
// ServeHTTP implements the http.Handler interface.
func (h ForgotPasswordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tnt, err := tenant.GetTenantFromContext(ctx)
if err != nil {
err = fmt.Errorf("getting tenant from context: %w", err)
httperror.Unauthorized("", err, nil).Render(w)
return
}
var forgotPasswordRequest ForgotPasswordRequest
err = json.NewDecoder(r.Body).Decode(&forgotPasswordRequest)
if err != nil {
httperror.BadRequest("invalid request body", err, nil).Render(w)
return
}
if !h.ReCAPTCHADisabled {
// validating reCAPTCHA Token
isValid, recaptchaErr := h.ReCAPTCHAValidator.IsTokenValid(ctx, forgotPasswordRequest.ReCAPTCHAToken)
if recaptchaErr != nil {
httperror.InternalError(ctx, "Cannot validate reCAPTCHA token", recaptchaErr, nil).Render(w)
return
}
if !isValid {
log.Ctx(ctx).Errorf("reCAPTCHA token is invalid for request with email %s", utils.TruncateString(forgotPasswordRequest.Email, 3))
httperror.BadRequest("reCAPTCHA token invalid", nil, nil).Render(w)
return
}
}
// validate request
v := validators.NewValidator()
v.Check(forgotPasswordRequest.Email != "", "email", "email is required")
if v.HasErrors() {
httperror.BadRequest("request invalid", err, v.Errors).Render(w)
return
}
resetToken, err := h.AuthManager.ForgotPassword(ctx, forgotPasswordRequest.Email)
// if we don't find the user by email, we just return an ok response
// to prevent malicious client from searching accounts in the system
if err != nil {
if errors.Is(err, auth.ErrUserNotFound) {
log.Ctx(ctx).Errorf("error in forgot password handler, email not found: %s", forgotPasswordRequest.Email)
} else if errors.Is(err, auth.ErrUserHasValidToken) {
log.Ctx(ctx).Errorf("error in forgot password handler, user has a valid token")
} else {
httperror.InternalError(ctx, "", err, nil).Render(w)
return
}
}
if err == nil {
organization, err := h.Models.Organizations.Get(ctx)
if err != nil {
err = fmt.Errorf("error getting organization data: %w", err)
httperror.InternalError(ctx, "", err, nil).Render(w)
return
}
resetPasswordLink, err := url.JoinPath(*tnt.SDPUIBaseURL, "reset-password")
if err != nil {
err = fmt.Errorf("error getting reset password link: %w", err)
log.Ctx(ctx).Error(err)
httperror.InternalError(ctx, "", err, nil).Render(w)
return
}
forgotPasswordData := htmltemplate.ForgotPasswordMessageTemplate{
ResetToken: resetToken,
ResetPasswordLink: resetPasswordLink,
OrganizationName: organization.Name,
}
messageContent, err := htmltemplate.ExecuteHTMLTemplateForForgotPasswordMessage(forgotPasswordData)
if err != nil {
err = fmt.Errorf("error executing forgot password message template: %w", err)
httperror.InternalError(ctx, "", err, nil).Render(w)
return
}
msg := message.Message{
ToEmail: forgotPasswordRequest.Email,
Title: forgotPasswordMessageTitle,
Message: messageContent,
}
err = h.MessengerClient.SendMessage(msg)
if err != nil {
err = fmt.Errorf("error sending forgot password email for email %s: %w", forgotPasswordRequest.Email, err)
httperror.InternalError(ctx, "", err, nil).Render(w)
return
}
}
responseBody := ForgotPasswordResponseBody{
Message: "Password reset requested. If the email is registered, you'll receive a reset link shortly. Check your inbox and spam folders.",
}
httpjson.RenderStatus(w, http.StatusOK, responseBody, httpjson.JSON)
}