package auth import ( "database/sql" "encoding/json" "errors" "net/http" "strings" "time" "github.com/SecurityBrewery/catalyst/app/auth/password" "github.com/SecurityBrewery/catalyst/app/database/sqlc" "github.com/SecurityBrewery/catalyst/app/mail" "github.com/SecurityBrewery/catalyst/app/settings" ) func handleResetPasswordMail(queries *sqlc.Queries, mailer *mail.Mailer) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { type passwordResetData struct { Email string `json:"email"` } b, err := json.Marshal(map[string]any{ "message": "Password reset email sent when the user exists", }) if err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to create response: "+err.Error()) return } var data passwordResetData if err := json.NewDecoder(r.Body).Decode(&data); err != nil { errorJSON(w, http.StatusBadRequest, "Invalid request, missing email field") return } user, err := queries.UserByEmail(r.Context(), &data.Email) if err != nil { if errors.Is(err, sql.ErrNoRows) { // Do not reveal whether the user exists or not w.WriteHeader(http.StatusOK) _, _ = w.Write(b) return } errorJSON(w, http.StatusInternalServerError, "Failed to get user: "+err.Error()) return } settings, err := settings.Load(r.Context(), queries) if err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to load settings: "+err.Error()) return } resetToken, err := createResetToken(&user, settings) if err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to create reset token: "+err.Error()) return } link := settings.Meta.AppURL + "/ui/password-reset?mail=" + data.Email + "&token=" + resetToken subject := settings.Meta.ResetPasswordTemplate.Subject subject = strings.ReplaceAll(subject, "{APP_NAME}", settings.Meta.AppName) plainTextBody := `Hello, Thank you for joining us at {APP_NAME}. Click on the link below to verify your email address or copy the token into the app: {ACTION_URL} Thanks, {APP_NAME} team` plainTextBody = strings.ReplaceAll(plainTextBody, "{ACTION_URL}", link) plainTextBody = strings.ReplaceAll(plainTextBody, "{APP_NAME}", settings.Meta.AppName) htmlBody := settings.Meta.ResetPasswordTemplate.Body htmlBody = strings.ReplaceAll(htmlBody, "{ACTION_URL}", link) htmlBody = strings.ReplaceAll(htmlBody, "{APP_NAME}", settings.Meta.AppName) if err := mailer.Send(r.Context(), data.Email, subject, plainTextBody, htmlBody); err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to send password reset email: "+err.Error()) return } w.WriteHeader(http.StatusOK) _, _ = w.Write(b) } } func handlePassword(queries *sqlc.Queries) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { type passwordResetData struct { Token string `json:"token"` Email string `json:"email"` Password string `json:"password"` PasswordConfirm string `json:"password_confirm"` } var data passwordResetData if err := json.NewDecoder(r.Body).Decode(&data); err != nil { errorJSON(w, http.StatusBadRequest, "Invalid request, missing email or password fields") return } if data.Password != data.PasswordConfirm { errorJSON(w, http.StatusBadRequest, "Passwords do not match") return } user, err := queries.UserByEmail(r.Context(), &data.Email) if err != nil { if errors.Is(err, sql.ErrNoRows) { errorJSON(w, http.StatusBadRequest, "Invalid or expired reset token") return } errorJSON(w, http.StatusInternalServerError, "Failed to get user: "+err.Error()) return } settings, err := settings.Load(r.Context(), queries) if err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to load settings: "+err.Error()) return } if err := verifyResetToken(data.Token, &user, settings.Meta.AppURL, settings.RecordPasswordResetToken.Secret); err != nil { errorJSON(w, http.StatusBadRequest, "Invalid or expired reset token: "+err.Error()) return } passwordHash, tokenKey, err := password.Hash(data.Password) if err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to hash password: "+err.Error()) return } now := time.Now().UTC() if _, err := queries.UpdateUser(r.Context(), sqlc.UpdateUserParams{ ID: user.ID, PasswordHash: &passwordHash, TokenKey: &tokenKey, LastResetSentAt: &now, }); err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to update password: "+err.Error()) return } b, err := json.Marshal(map[string]any{ "message": "Password reset successfully", }) if err != nil { errorJSON(w, http.StatusInternalServerError, "Failed to create response: "+err.Error()) return } w.WriteHeader(http.StatusOK) _, _ = w.Write(b) } }