feat(auth): add password reset #178
Some checks are pending
Build Docker Image / Explore-Gitea-Actions (push) Successful in 46s
Build and Push Docker Image / Explore-Gitea-Actions (push) Waiting to run

This commit was merged in pull request #180.
This commit is contained in:
2024-09-13 15:49:30 +02:00
parent 6c1edcd0a8
commit 63ddf77d6a
7 changed files with 196 additions and 10 deletions

View File

@@ -7,8 +7,10 @@ import (
"database/sql"
"encoding/base64"
"errors"
"log/slog"
"net/http"
"net/mail"
"net/url"
"strings"
"time"
@@ -149,12 +151,32 @@ func HandleSignUpVerifyResponsePage(db *sql.DB) http.HandlerFunc {
func HandleChangePasswordPage(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
isPasswordReset := r.URL.Query().Has("token")
user := utils.GetUserFromSession(db, r)
if user == nil {
if user == nil && !isPasswordReset {
utils.DoRedirect(w, r, "/auth/signin")
} else {
userComp := UserInfoComp(user)
comp := auth.ChangePasswordComp()
comp := auth.ChangePasswordComp(isPasswordReset)
err := template.Layout(comp, userComp).Render(r.Context(), w)
if err != nil {
utils.LogError("Failed to render change password page", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
}
}
func HandleResetPasswordPage(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUserFromSession(db, r)
if user != nil {
utils.DoRedirect(w, r, "/auth/signin")
} else {
userComp := UserInfoComp(nil)
comp := auth.ResetPasswordComp()
err := template.Layout(comp, userComp).Render(r.Context(), w)
if err != nil {
utils.LogError("Failed to render change password page", err)
@@ -402,6 +424,7 @@ func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc {
func HandleChangePasswordComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUserFromSession(db, r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
@@ -453,6 +476,118 @@ func HandleChangePasswordComp(db *sql.DB) http.HandlerFunc {
}
}
func HandleActualResetPasswordComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
pageUrl, err := url.Parse(r.Header.Get("HX-Current-URL"))
if err != nil {
utils.LogError("Could not get current URL", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
token := pageUrl.Query().Get("token")
if token == "" {
utils.TriggerToast(w, r, "error", "No token")
return
}
newPass := r.FormValue("new-password")
err = checkPassword(newPass)
if err != nil {
utils.TriggerToast(w, r, "error", err.Error())
return
}
var (
userId uuid.UUID
salt []byte
)
err = db.QueryRow(`
SELECT u.user_uuid, salt
FROM user_token t
INNER JOIN user u ON t.user_uuid = u.user_uuid
WHERE t.token = ?
AND t.type = 'password_reset'
AND t.expires_at > datetime()
`, token).Scan(&userId, &salt)
if err != nil {
slog.Warn("Could not get user from token: " + err.Error())
utils.TriggerToast(w, r, "error", "Invalid token")
return
}
_, err = db.Exec("DELETE FROM user_token WHERE token = ? AND type = 'password_reset'", token)
if err != nil {
utils.LogError("Could not delete token", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
passHash := getHashPassword(newPass, salt)
_, err = db.Exec("UPDATE user SET password = ? WHERE user_uuid = ?", passHash, userId)
if err != nil {
utils.LogError("Could not update password", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
utils.TriggerToast(w, r, "success", "Password changed")
}
}
func HandleResetPasswordComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
if email == "" {
utils.TriggerToast(w, r, "error", "Please enter an email")
return
}
var b []byte = make([]byte, 32)
_, err := rand.Reader.Read(b)
if err != nil {
utils.LogError("Could not generate token", err)
return
}
token := base64.StdEncoding.EncodeToString(b)
res, err := db.Exec(`
INSERT INTO user_token (user_uuid, type, token, created_at, expires_at)
SELECT user_uuid, 'password_reset', ?, datetime(), datetime('now', '+15 minute')
FROM user
WHERE email = ?
`, token, email)
if err != nil {
utils.LogError("Could not insert token", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
i, err := res.RowsAffected()
if err != nil {
utils.LogError("Could not get rows affected", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
if i != 0 {
var mail strings.Builder
err = tempMail.ResetPassword(token).Render(context.Background(), &mail)
if err != nil {
utils.LogError("Could not render reset password email", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
utils.SendMail(email, "Reset Password", mail.String())
}
utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent")
}
}
func sendVerificationEmail(db *sql.DB, userId string, email string) {
var token string