mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-20 14:03:07 +01:00
refactor: remove pocketbase (#1138)
This commit is contained in:
178
app/auth/resetpassword.go
Normal file
178
app/auth/resetpassword.go
Normal file
@@ -0,0 +1,178 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user