diff --git a/handler.go b/handler.go index 4a59910..91097c8 100644 --- a/handler.go +++ b/handler.go @@ -34,12 +34,15 @@ func getHandler(db *sql.DB) http.Handler { router.Handle("/auth/delete-account", service.HandleDeleteAccountPage(db)) router.Handle("/auth/verify-email", service.HandleSignUpVerifyResponsePage(db)) // The link contained in the email router.Handle("/auth/change-password", service.HandleChangePasswordPage(db)) + router.Handle("/auth/reset-password", service.HandleResetPasswordPage(db)) router.Handle("/api/auth/signup", service.HandleSignUpComp(db)) router.Handle("/api/auth/signin", service.HandleSignInComp(db)) router.Handle("/api/auth/signout", service.HandleSignOutComp(db)) router.Handle("/api/auth/delete-account", service.HandleDeleteAccountComp(db)) router.Handle("/api/auth/verify-resend", service.HandleVerifyResendComp(db)) router.Handle("/api/auth/change-password", service.HandleChangePasswordComp(db)) + router.Handle("/api/auth/reset-password", service.HandleResetPasswordComp(db)) + router.Handle("/api/auth/reset-password-actual", service.HandleActualResetPasswordComp(db)) return middleware.Logging(middleware.EnableCors(router)) } diff --git a/service/auth.go b/service/auth.go index add4e03..6ff5ec1 100644 --- a/service/auth.go +++ b/service/auth.go @@ -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 diff --git a/template/auth/change_password.templ b/template/auth/change_password.templ index 2d56221..786ef7a 100644 --- a/template/auth/change_password.templ +++ b/template/auth/change_password.templ @@ -1,17 +1,23 @@ package auth -templ ChangePasswordComp() { +templ ChangePasswordComp(isPasswordReset bool) {

Change Password

- + if !isPasswordReset { + + } diff --git a/template/auth/reset_password.templ b/template/auth/reset_password.templ new file mode 100644 index 0000000..89c87a8 --- /dev/null +++ b/template/auth/reset_password.templ @@ -0,0 +1,19 @@ +package auth + +templ ResetPasswordComp() { + +

+ Reset Password +

+ + +
+} diff --git a/template/auth/sign_in_or_up.templ b/template/auth/sign_in_or_up.templ index 2f69a83..81f3786 100644 --- a/template/auth/sign_in_or_up.templ +++ b/template/auth/sign_in_or_up.templ @@ -50,8 +50,9 @@ templ SignInOrUpComp(isSignIn bool) {
if isSignIn { + Forgot Password? Don't have an account? Sign Up - } else { diff --git a/template/mail/register.templ b/template/mail/register.templ index 6072314..3d52cf6 100644 --- a/template/mail/register.templ +++ b/template/mail/register.templ @@ -5,7 +5,7 @@ import ( "net/url" ) -templ Register(mailCode string) { +templ Register(token string) { @@ -15,7 +15,7 @@ templ Register(mailCode string) {

Thank you for Sign Up!

-

Click + + + + + Reset Password + + +

Reset your password

+

Click