diff --git a/handler.go b/handler.go index 203b205..e4faa5b 100644 --- a/handler.go +++ b/handler.go @@ -13,6 +13,10 @@ import ( func getHandler(db *sql.DB) http.Handler { var router = http.NewServeMux() + if utils.Environment == "dev" { + router.HandleFunc("/mail/", handleMails) + } + router.HandleFunc("/", service.HandleIndexAnd404(db)) // Serve static files (CSS, JS and images) @@ -33,19 +37,15 @@ func getHandler(db *sql.DB) http.Handler { router.Handle("/api/auth/signout", service.HandleSignOutComp(db)) router.Handle("/api/auth/verify-resend", service.HandleVerifyResendComp(db)) - if utils.Environment == "dev" { - router.HandleFunc("/mail/", handleMails) - } - return middleware.Logging(middleware.EnableCors(router)) } +func auth(db *sql.DB, h http.Handler) http.Handler { + return middleware.EnsureValidSession(db, h) +} + func handleMails(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/mail/register" { mail.Register("test-code").Render(r.Context(), w) } } - -func auth(db *sql.DB, h http.Handler) http.Handler { - return middleware.EnsureValidSession(db, h) -} diff --git a/middleware/auth.go b/middleware/auth.go index e458f2b..f0dd4db 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -22,7 +22,7 @@ func EnsureValidSession(db *sql.DB, next http.Handler) http.Handler { // } user := utils.GetUserFromSession(db, r) - if user == nil || !user.SessionValid { + if user == nil { utils.DoRedirect(w, r, "/auth/signin") return } @@ -45,7 +45,7 @@ func EnsureValidSession(db *sql.DB, next http.Handler) http.Handler { // // sessionId := getSessionID(r) // user := verifySession(db, sessionId) -// if user == nil || !user.SessionValid { +// if user == nil { // return true, false // } // diff --git a/service/auth.go b/service/auth.go index 3853e9d..d9054fc 100644 --- a/service/auth.go +++ b/service/auth.go @@ -24,17 +24,21 @@ import ( func HandleSignInPage(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) - if user == nil || !user.SessionValid { + + if user == nil { userComp := UserInfoComp(nil) signIn := auth.SignInOrUpComp(true) - template.Layout(signIn, userComp).Render(r.Context(), w) - return + err := template.Layout(signIn, userComp).Render(r.Context(), w) + + if err != nil { + utils.LogError("Failed to render sign in page", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + } else if !user.EmailVerified { utils.DoRedirect(w, r, "/auth/verify") - return } else { utils.DoRedirect(w, r, "/") - return } } } @@ -42,17 +46,21 @@ func HandleSignInPage(db *sql.DB) http.HandlerFunc { func HandleSignUpPage(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) - if user == nil || !user.SessionValid { + + if user == nil { userComp := UserInfoComp(nil) signUpComp := auth.SignInOrUpComp(false) - template.Layout(signUpComp, userComp).Render(r.Context(), w) - return + err := template.Layout(signUpComp, userComp).Render(r.Context(), w) + + if err != nil { + utils.LogError("Failed to render sign up page", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + } else if !user.EmailVerified { utils.DoRedirect(w, r, "/auth/verify") - return } else { utils.DoRedirect(w, r, "/") - return } } } @@ -60,17 +68,18 @@ func HandleSignUpPage(db *sql.DB) http.HandlerFunc { func HandleSignUpVerifyPage(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) - if user == nil || !user.SessionValid { + if user == nil { utils.DoRedirect(w, r, "/auth/signin") - return - } - if user.EmailVerified { + } else if user.EmailVerified { utils.DoRedirect(w, r, "/") - return } else { userComp := UserInfoComp(user) signIn := auth.VerifyComp() - template.Layout(signIn, userComp).Render(r.Context(), w) + err := template.Layout(signIn, userComp).Render(r.Context(), w) + if err != nil { + utils.LogError("Failed to render verify page", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } } } } @@ -95,12 +104,15 @@ func HandleSignUpVerifyResponsePage(db *sql.DB) http.HandlerFunc { `, token) if err != nil { - utils.LogError("Could not update user", err) + utils.LogError("Could not update user on verify response", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } + i, err := result.RowsAffected() if err != nil { - utils.LogError("Could not get rows affected", err) + utils.LogError("Could not get rows affected on verify response", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } @@ -170,8 +182,8 @@ func HandleSignUpComp(db *sql.DB) http.HandlerFunc { return } - auth.Error("Internal Server Error").Render(r.Context(), w) utils.LogError("Could not insert user", err) + auth.Error("Internal Server Error").Render(r.Context(), w) return } @@ -249,7 +261,8 @@ func HandleSignOutComp(db *sql.DB) http.HandlerFunc { if user != nil { _, err := db.Exec("DELETE FROM session WHERE session_id = ?", user.SessionId) if err != nil { - utils.LogError("Could not delete session%v", err) + utils.LogError("Could not delete session", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -266,19 +279,19 @@ func HandleSignOutComp(db *sql.DB) http.HandlerFunc { } http.SetCookie(w, &c) - w.Header().Add("HX-Redirect", "/") + utils.DoRedirect(w, r, "/") } } func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) - if user == nil || !user.SessionValid || user.EmailVerified { + if user == nil || user.EmailVerified { utils.DoRedirect(w, r, "/auth/signin") return } - sendVerificationEmail(db, r, user.Id.String(), user.Email) + go sendVerificationEmail(db, r, user.Id.String(), user.Email) w.Write([]byte("
Verification email sent
")) } diff --git a/service/index_and_404.go b/service/index_and_404.go index 050bfb4..480ca6d 100644 --- a/service/index_and_404.go +++ b/service/index_and_404.go @@ -23,6 +23,10 @@ func HandleIndexAnd404(db *sql.DB) http.HandlerFunc { comp = template.Layout(template.Index(), userComp) } - comp.Render(r.Context(), w) + err := comp.Render(r.Context(), w) + if err != nil { + utils.LogError("Failed to render index", err) + http.Error(w, "Failed to render index", http.StatusInternalServerError) + } } } diff --git a/service/workout.go b/service/workout.go index a43cea6..40818b0 100644 --- a/service/workout.go +++ b/service/workout.go @@ -1,6 +1,7 @@ package service import ( + "log/slog" "me-fit/template" "me-fit/template/workout" "me-fit/utils" @@ -22,7 +23,11 @@ func HandleWorkoutPage(db *sql.DB) http.HandlerFunc { currentDate := time.Now().Format("2006-01-02") inner := workout.WorkoutComp(currentDate) userComp := UserInfoComp(user) - template.Layout(inner, userComp).Render(r.Context(), w) + err := template.Layout(inner, userComp).Render(r.Context(), w) + if err != nil { + utils.LogError("Failed to render workout page", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } } } @@ -40,24 +45,26 @@ func HandleWorkoutNewComp(db *sql.DB) http.HandlerFunc { var repsStr = r.FormValue("reps") if dateStr == "" || typeStr == "" || setsStr == "" || repsStr == "" { + utils.TriggerToast(w, r, "error", "Missing required fields") http.Error(w, "Missing required fields", http.StatusBadRequest) return } date, err := time.Parse("2006-01-02", dateStr) if err != nil { + utils.TriggerToast(w, r, "error", "Invalid date") http.Error(w, err.Error(), http.StatusBadRequest) return } - sets, err := strconv.Atoi(setsStr) if err != nil { + utils.TriggerToast(w, r, "error", "Invalid number") http.Error(w, err.Error(), http.StatusBadRequest) return } - reps, err := strconv.Atoi(repsStr) if err != nil { + utils.TriggerToast(w, r, "error", "Invalid number") http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -66,9 +73,11 @@ func HandleWorkoutNewComp(db *sql.DB) http.HandlerFunc { err = db.QueryRow("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?) RETURNING rowid", user.Id, date, typeStr, sets, reps).Scan(&rowId) if err != nil { utils.LogError("Could not insert workout", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") http.Error(w, err.Error(), http.StatusInternalServerError) return } + wo := workout.Workout{ Id: strconv.Itoa(rowId), Date: renderDate(date), @@ -77,8 +86,12 @@ func HandleWorkoutNewComp(db *sql.DB) http.HandlerFunc { Reps: r.FormValue("reps"), } - w.Header().Set("HX-Trigger", `{"toast": "none|Workout added"}`) - workout.WorkoutItemComp(wo, true).Render(r.Context(), w) + err = workout.WorkoutItemComp(wo, true).Render(r.Context(), w) + if err != nil { + utils.LogError("Could not render workoutitem", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") + http.Error(w, err.Error(), http.StatusInternalServerError) + } } } @@ -92,6 +105,8 @@ func HandleWorkoutGetComp(db *sql.DB) http.HandlerFunc { rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ? ORDER BY date desc", user.Id) if err != nil { + utils.LogError("Could not get workouts", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -102,12 +117,16 @@ func HandleWorkoutGetComp(db *sql.DB) http.HandlerFunc { err = rows.Scan(&workout.Id, &workout.Date, &workout.Type, &workout.Sets, &workout.Reps) if err != nil { + utils.LogError("Could not scan workout", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") http.Error(w, err.Error(), http.StatusInternalServerError) return } workout.Date, err = renderDateStr(workout.Date) if err != nil { + utils.LogError("Could not render date", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -130,23 +149,31 @@ func HandleWorkoutDeleteComp(db *sql.DB) http.HandlerFunc { rowId := r.PathValue("id") if rowId == "" { http.Error(w, "Missing required fields", http.StatusBadRequest) + slog.Warn("Missing required fields for workout delete") + utils.TriggerToast(w, r, "error", "Missing ID field") return } res, err := db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", user.Id, rowId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) + utils.LogError("Could not delete workout", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") return } rows, err := res.RowsAffected() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) + utils.LogError("Could not get rows affected", err) + utils.TriggerToast(w, r, "error", "Internal Server Error") return } if rows == 0 { http.Error(w, "Not found", http.StatusNotFound) + slog.Warn("Could not find workout to delete") + utils.TriggerToast(w, r, "error", "Not found. Refresh the page.") return } } diff --git a/types/types.go b/types/types.go index aba8a09..2c5324f 100644 --- a/types/types.go +++ b/types/types.go @@ -7,5 +7,4 @@ type User struct { Email string SessionId string EmailVerified bool - SessionValid bool } diff --git a/utils/http.go b/utils/http.go index 72f1a79..406d0de 100644 --- a/utils/http.go +++ b/utils/http.go @@ -2,6 +2,7 @@ package utils import ( "database/sql" + "fmt" "log/slog" "me-fit/types" "net/http" @@ -35,9 +36,16 @@ func LogErrorMsg(message string) { errorMetric.Inc() } +func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message string) { + if isHtmx(r) { + w.Header().Set("HX-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, message)) + } else { + LogErrorMsg("Trying to trigger toast in non-HTMX request") + } +} + func DoRedirect(w http.ResponseWriter, r *http.Request, url string) { - isHtmx := r.Header.Get("HX-Request") == "true" - if isHtmx { + if isHtmx(r) { w.Header().Add("HX-Redirect", url) } else { http.Redirect(w, r, url, http.StatusSeeOther) @@ -75,12 +83,11 @@ func GetUserFromSession(db *sql.DB, r *http.Request) *types.User { } if createdAt.Add(time.Duration(8 * time.Hour)).Before(time.Now()) { - user.SessionValid = false + return nil } else { - user.SessionValid = true + return &user } - return &user } func getSessionID(r *http.Request) string { @@ -91,3 +98,7 @@ func getSessionID(r *http.Request) string { } return "" } + +func isHtmx(r *http.Request) bool { + return r.Header.Get("HX-Request") == "true" +}