diff --git a/handler/auth.go b/handler/auth.go index 44c2135..ed9a9ef 100644 --- a/handler/auth.go +++ b/handler/auth.go @@ -1,14 +1,18 @@ package handler import ( + "context" "me-fit/service" "me-fit/template" "me-fit/template/auth" + mail "me-fit/template/mail" "me-fit/types" "me-fit/utils" + "strings" "database/sql" "net/http" + "net/url" ) func authUi(db *sql.DB) http.Handler { @@ -31,12 +35,12 @@ func authApi(db *sql.DB) http.Handler { router.Handle("/api/auth/signup", handleSignUp(db)) router.Handle("/api/auth/signin", handleSignIn(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)) + router.Handle("/api/auth/signout", handleSignOut(db)) + router.Handle("/api/auth/delete-account", handleDeleteAccount(db)) + router.Handle("/api/auth/verify-resend", handleVerifyResend(db)) + router.Handle("/api/auth/change-password", handleChangePassword(db)) + router.Handle("/api/auth/reset-password", handleResetPassword(db)) + router.Handle("/api/auth/reset-password-actual", handleActualResetPassword(db)) return router } @@ -226,3 +230,140 @@ func handleSignIn(db *sql.DB) http.HandlerFunc { utils.DoRedirect(w, r, "/auth/verify") } } + +func handleSignOut(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := service.GetUserFromRequest(db, r) + + err := service.SignOut(db, user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + utils.TriggerToast(w, r, "error", "Internal Server Error") + return + } + + c := http.Cookie{ + Name: "id", + Value: "", + MaxAge: -1, + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Path: "/", + } + + http.SetCookie(w, &c) + utils.DoRedirect(w, r, "/") + } +} + +func handleDeleteAccount(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := service.GetUserFromRequest(db, r) + + if user == nil { + utils.DoRedirect(w, r, "/auth/signin") + return + } + + password := r.FormValue("password") + + err := service.DeleteAccount(db, user, password) + if err != nil { + utils.LogError("Could not delete account", err) + utils.TriggerToast(w, r, "error", err.Error()) + } + + utils.DoRedirect(w, r, "/") + } +} + +func handleVerifyResend(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := service.GetUserFromRequest(db, r) + if user == nil || user.EmailVerified { + utils.DoRedirect(w, r, "/auth/signin") + return + } + + go service.SendVerificationEmail(db, user.Id.String(), user.Email) + + w.Write([]byte("

Verification email sent

")) + } +} + +func handleChangePassword(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + user := service.GetUserFromRequest(db, r) + if user == nil { + utils.DoRedirect(w, r, "/auth/signin") + return + } + + currPass := r.FormValue("current-password") + newPass := r.FormValue("new-password") + + err := service.ChangePassword(db, user, currPass, newPass) + if err != nil { + utils.TriggerToast(w, r, "error", err.Error()) + return + } + + utils.TriggerToast(w, r, "success", "Password changed") + } +} + +func handleResetPassword(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + email := r.FormValue("email") + + token, err := service.ResetPassword(db, email) + if err != nil { + utils.TriggerToast(w, r, "error", err.Error()) + return + } + + if token != "" { + var string strings.Builder + err = mail.ResetPassword(token).Render(context.Background(), &string) + 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", string.String()) + } + + utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent") + } +} + +func handleActualResetPassword(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") + + service.ActualResetPassword(db, token, newPass) + if err != nil { + utils.TriggerToast(w, r, "error", err.Error()) + return + } + + utils.TriggerToast(w, r, "success", "Password changed") + } +} diff --git a/service/auth.go b/service/auth.go index d0f1d84..27312f3 100644 --- a/service/auth.go +++ b/service/auth.go @@ -9,7 +9,6 @@ import ( "log/slog" "net/http" "net/mail" - "net/url" "strings" "time" @@ -70,7 +69,7 @@ func SignUp(db *sql.DB, email string, password string) (*types.SessionId, error) } // Send verification email as a goroutine - go sendVerificationEmail(db, userId.String(), email) + go SendVerificationEmail(db, userId.String(), email) return sessionId, nil } @@ -182,199 +181,113 @@ func UserInfoComp(user *types.User) templ.Component { } } -func HandleSignOutComp(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := GetUserFromRequest(db, r) - - if user != nil { - _, err := db.Exec("DELETE FROM session WHERE session_id = ?", user.SessionId) - if err != nil { - utils.LogError("Could not delete session", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } - - c := http.Cookie{ - Name: "id", - Value: "", - MaxAge: -1, - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - Path: "/", - } - - http.SetCookie(w, &c) - utils.DoRedirect(w, r, "/") +func SignOut(db *sql.DB, user *types.User) error { + if user == nil { + return nil } + + _, err := db.Exec("DELETE FROM session WHERE session_id = ?", user.SessionId) + if err != nil { + return errors.Join(errors.New("Could not delete session"), err) + } + + return nil } -func HandleDeleteAccountComp(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := GetUserFromRequest(db, r) - if user == nil { - utils.DoRedirect(w, r, "/auth/signin") - return - } +func DeleteAccount(db *sql.DB, user *types.User, password string) error { - password := r.FormValue("password") - if password == "" { - utils.TriggerToast(w, r, "error", "Password is required") - return - } - - var ( - storedHash []byte - salt []byte - ) - - err := db.QueryRow("SELECT password, salt FROM user WHERE user_uuid = ?", user.Id).Scan(&storedHash, &salt) - if err != nil { - utils.LogError("Could not get password", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - return - } - - currHash := getHashPassword(password, salt) - if subtle.ConstantTimeCompare(currHash, storedHash) == 0 { - utils.TriggerToast(w, r, "error", "Password is not correct") - return - } - - _, err = db.Exec("DELETE FROM workout WHERE user_id = ?", user.Id) - if err != nil { - utils.LogError("Could not delete workouts", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - return - } - - _, err = db.Exec("DELETE FROM user_token WHERE user_uuid = ?", user.Id) - if err != nil { - utils.LogError("Could not delete user tokens", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - return - } - - _, err = db.Exec("DELETE FROM session WHERE user_uuid = ?", user.Id) - if err != nil { - utils.LogError("Could not delete sessions", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - return - } - - _, err = db.Exec("DELETE FROM user WHERE user_uuid = ?", user.Id) - if err != nil { - utils.LogError("Could not delete user", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - return - } - - go utils.SendMail(user.Email, "Account deleted", "Your account has been deleted") - - utils.DoRedirect(w, r, "/") + if password == "" { + return errors.New("Please enter your password") } + + var ( + storedHash []byte + salt []byte + ) + + err := db.QueryRow("SELECT password, salt FROM user WHERE user_uuid = ?", user.Id).Scan(&storedHash, &salt) + if err != nil { + return errors.Join(errors.New("Could not get password"), err) + } + + currHash := getHashPassword(password, salt) + if subtle.ConstantTimeCompare(currHash, storedHash) == 0 { + return errors.New("Password is not correct") + } + + _, err = db.Exec("DELETE FROM workout WHERE user_id = ?", user.Id) + if err != nil { + return errors.Join(errors.New("Could not delete workouts"), err) + } + + _, err = db.Exec("DELETE FROM user_token WHERE user_uuid = ?", user.Id) + if err != nil { + return errors.Join(errors.New("Could not delete tokens"), err) + } + + _, err = db.Exec("DELETE FROM session WHERE user_uuid = ?", user.Id) + if err != nil { + return errors.Join(errors.New("Could not delete sessions"), err) + } + + _, err = db.Exec("DELETE FROM user WHERE user_uuid = ?", user.Id) + if err != nil { + return errors.Join(errors.New("Could not delete user"), err) + } + + go utils.SendMail(user.Email, "Account deleted", "Your account has been deleted") + return nil } -func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := GetUserFromRequest(db, r) - if user == nil || user.EmailVerified { - utils.DoRedirect(w, r, "/auth/signin") - return - } +func ChangePassword(db *sql.DB, user *types.User, currPass string, newPass string) error { - go sendVerificationEmail(db, user.Id.String(), user.Email) - - w.Write([]byte("

Verification email sent

")) + err := checkPassword(newPass) + if err != nil { + return err } + + if currPass == newPass { + return errors.New("New password can not be the same as the current password") + } + + var ( + storedHash []byte + salt []byte + ) + + err = db.QueryRow("SELECT password, salt FROM user WHERE user_uuid = ?", user.Id).Scan(&storedHash, &salt) + if err != nil { + return errors.Join(errors.New("Could not get password"), err) + } + + currHash := getHashPassword(currPass, salt) + if subtle.ConstantTimeCompare(currHash, storedHash) == 0 { + return errors.New("Current password is not correct") + } + + newHash := getHashPassword(newPass, salt) + + _, err = db.Exec("UPDATE user SET password = ? WHERE user_uuid = ?", newHash, user.Id) + if err != nil { + return errors.Join(errors.New("Could not update password"), err) + } + + return nil } -func HandleChangePasswordComp(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func ActualResetPassword(db *sql.DB, token string, newPass string) error { - user := GetUserFromRequest(db, r) - if user == nil { - utils.DoRedirect(w, r, "/auth/signin") - return - } - - currPass := r.FormValue("current-password") - newPass := r.FormValue("new-password") - - err := checkPassword(newPass) - if err != nil { - utils.TriggerToast(w, r, "error", err.Error()) - return - } - - if currPass == newPass { - utils.TriggerToast(w, r, "error", "Please use a new password") - return - } - - var ( - storedHash []byte - salt []byte - ) - - err = db.QueryRow("SELECT password, salt FROM user WHERE user_uuid = ?", user.Id).Scan(&storedHash, &salt) - if err != nil { - utils.LogError("Could not get password", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - return - } - - currHash := getHashPassword(currPass, salt) - if subtle.ConstantTimeCompare(currHash, storedHash) == 0 { - utils.TriggerToast(w, r, "error", "Current Password is not correct") - return - } - - newHash := getHashPassword(newPass, salt) - - _, err = db.Exec("UPDATE user SET password = ? WHERE user_uuid = ?", newHash, user.Id) - 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") + err := checkPassword(newPass) + if err != nil { + return err } -} -func HandleActualResetPasswordComp(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { + var ( + userId uuid.UUID + salt []byte + ) - 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(` + err = db.QueryRow(` SELECT u.user_uuid, salt FROM user_token t INNER JOIN user u ON t.user_uuid = u.user_uuid @@ -382,81 +295,59 @@ func HandleActualResetPasswordComp(db *sql.DB) http.HandlerFunc { 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") + if err != nil { + return errors.Join(errors.New("Could not get user from token"), err) } + + _, err = db.Exec("DELETE FROM user_token WHERE token = ? AND type = 'password_reset'", token) + if err != nil { + return errors.Join(errors.New("Could not delete token"), err) + } + + passHash := getHashPassword(newPass, salt) + + _, err = db.Exec("UPDATE user SET password = ? WHERE user_uuid = ?", passHash, userId) + if err != nil { + return errors.Join(errors.New("Could not update password"), err) + } + + return nil } -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 - } +func ResetPassword(db *sql.DB, email string) (string, error) { - token, err := utils.RandomToken() - if err != nil { - utils.LogError("Could not generate token", err) - return - } + if email == "" { + return "", errors.New("Please enter an email") + } - res, err := db.Exec(` + token, err := utils.RandomToken() + if err != nil { + return "", errors.Join(errors.New("Could not generate token"), err) + } + + 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 - } + if err != nil { + return "", errors.Join(errors.New("Could not insert token"), err) + } - i, err := res.RowsAffected() - if err != nil { - utils.LogError("Could not get rows affected", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") - return - } + i, err := res.RowsAffected() + if err != nil { + return "", errors.Join(errors.New("Could not get rows affected"), err) + } - 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") + if i == 0 { + return "", nil + } else { + return token, nil } } -func sendVerificationEmail(db *sql.DB, userId string, email string) { +func SendVerificationEmail(db *sql.DB, userId string, email string) { var token string err := db.QueryRow("SELECT token FROM user_token WHERE user_uuid = ? AND type = 'email_verify'", userId).Scan(&token)