diff --git a/Dockerfile b/Dockerfile index e4b1e11..62eeec2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ WORKDIR /me-fit RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env COPY --from=builder_go /me-fit/me-fit ./me-fit COPY --from=builder_node /me-fit/static ./static -COPY migrations ./migrations +COPY migration ./migration EXPOSE 8080 ENTRYPOINT ["/me-fit/me-fit"] diff --git a/handler.go b/handler.go index 7098725..1fa24bc 100644 --- a/handler.go +++ b/handler.go @@ -16,15 +16,15 @@ func getHandler(db *sql.DB) http.Handler { // Serve static files (CSS, JS and images) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) - router.HandleFunc("/app", service.HandleWorkoutPage(db)) - router.HandleFunc("POST /api/workout", service.HandleNewWorkout(db)) - router.HandleFunc("GET /api/workout", service.HandleGetWorkouts(db)) - router.HandleFunc("DELETE /api/workout/{id}", service.HandleDeleteWorkout(db)) + router.HandleFunc("/workout", service.HandleWorkoutPage(db)) + router.HandleFunc("POST /api/workout", service.HandleWorkoutNewComp(db)) + router.HandleFunc("GET /api/workout", service.HandleWorkoutGetComp(db)) + router.HandleFunc("DELETE /api/workout/{id}", service.HandleWorkoutDeleteComp(db)) router.HandleFunc("/auth/signin", service.HandleSignInPage(db)) router.HandleFunc("/auth/signup", service.HandleSignUpPage(db)) - router.HandleFunc("/api/auth/signup", service.HandleSignUp(db)) - router.HandleFunc("/api/auth/signin", service.HandleSignIn(db)) + router.HandleFunc("/api/auth/signup", service.HandleSignUpComp(db)) + router.HandleFunc("/api/auth/signin", service.HandleSignInComp(db)) router.HandleFunc("/api/auth/signout", service.HandleSignOutComp(db)) return middleware.Logging(middleware.EnableCors(router)) diff --git a/main.go b/main.go index eb9fd34..b567da9 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,11 @@ package main import ( + "log" "me-fit/utils" "database/sql" - "log" + "log/slog" "net/http" "github.com/joho/godotenv" @@ -13,7 +14,7 @@ import ( ) func main() { - log.Println("Starting server...") + slog.Info("Starting server...") err := godotenv.Load() if err != nil { @@ -33,7 +34,7 @@ func main() { Handler: promhttp.Handler(), } go func() { - log.Println("Starting prometheus server at", prometheusServer.Addr) + slog.Info("Starting prometheus server on " + prometheusServer.Addr) err := prometheusServer.ListenAndServe() if err != nil { panic(err) @@ -44,7 +45,7 @@ func main() { Addr: ":8080", Handler: getHandler(db), } - log.Println("Starting server at", server.Addr) + slog.Info("Starting server on " + server.Addr) err = server.ListenAndServe() if err != nil { diff --git a/middleware/cors.go b/middleware/cors.go index 104e73f..05ac4ac 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -2,6 +2,7 @@ package middleware import ( "log" + "log/slog" "net/http" "os" ) @@ -11,12 +12,11 @@ func EnableCors(next http.Handler) http.Handler { if base_url == "" { log.Fatal("BASE_URL is not set") } - log.Println("BASE_URL is", base_url) + slog.Info("BASE_URL is " + base_url) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", base_url) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) diff --git a/middleware/logger.go b/middleware/logger.go index a65cae3..6b55d99 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -1,9 +1,23 @@ package middleware import ( - "log" + "log/slog" "net/http" + "strconv" "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + metrics = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "mefit_request_total", + Help: "The total number of requests processed", + }, + []string{"path", "method", "status"}, + ) ) type WrappedWriter struct { @@ -25,6 +39,8 @@ func Logging(next http.Handler) http.Handler { StatusCode: http.StatusOK, } next.ServeHTTP(wrapped, r) - log.Println(r.RemoteAddr, wrapped.StatusCode, r.Method, r.URL.Path, time.Since(start)) + + slog.Info(r.RemoteAddr + " " + strconv.Itoa(wrapped.StatusCode) + " " + r.Method + " " + r.URL.Path + " " + time.Since(start).String()) + metrics.WithLabelValues(r.URL.Path, r.Method, http.StatusText(wrapped.StatusCode)).Inc() }) } diff --git a/migrations/001_initial_schema.up.sql b/migration/001_initial_schema.up.sql similarity index 100% rename from migrations/001_initial_schema.up.sql rename to migration/001_initial_schema.up.sql diff --git a/migrations/002_user_and_session.up.sql b/migration/002_user_and_session.up.sql similarity index 100% rename from migrations/002_user_and_session.up.sql rename to migration/002_user_and_session.up.sql diff --git a/service/auth.go b/service/auth.go index 55edcc0..faca684 100644 --- a/service/auth.go +++ b/service/auth.go @@ -5,7 +5,7 @@ import ( "crypto/subtle" "database/sql" "encoding/base64" - "log" + "log/slog" "net/http" "net/mail" "strings" @@ -20,8 +20,8 @@ import ( ) type User struct { - user_uuid uuid.UUID - email string + id uuid.UUID + email string } func HandleSignInPage(db *sql.DB) http.HandlerFunc { @@ -49,7 +49,7 @@ func UserInfoComp(user *User) templ.Component { } } -func HandleSignUp(db *sql.DB) http.HandlerFunc { +func HandleSignUpComp(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var email = r.FormValue("email") var password = r.FormValue("password") @@ -69,9 +69,9 @@ func HandleSignUp(db *sql.DB) http.HandlerFunc { return } - user_uuid, err := uuid.NewRandom() + userId, err := uuid.NewRandom() if err != nil { - log.Printf("Could not generate UUID: %v", err) + slog.Error("Could not generate UUID: %v", err) auth.Error("Internal Server Error").Render(r.Context(), w) return } @@ -79,14 +79,14 @@ func HandleSignUp(db *sql.DB) http.HandlerFunc { salt := make([]byte, 16) _, err = rand.Read(salt) if err != nil { - log.Printf("Could not generate salt: %v", err) + slog.Error("Could not generate salt: %v", err) auth.Error("Internal Server Error").Render(r.Context(), w) return } hash := getHashPassword(password, salt) - _, err = db.Exec("INSERT INTO user (user_uuid, email, email_verified, is_admin, password, salt, created_at) VALUES (?, ?, FALSE, FALSE, ?, ?, datetime())", user_uuid, email, hash, salt) + _, err = db.Exec("INSERT INTO user (user_uuid, email, email_verified, is_admin, password, salt, created_at) VALUES (?, ?, FALSE, FALSE, ?, ?, datetime())", userId, email, hash, salt) if err != nil { // This does leak information about the email being in use, though not specifically stated // It needs to be refacoteres to "If the email is not already in use, an email has been send to your address", or something @@ -99,21 +99,20 @@ func HandleSignUp(db *sql.DB) http.HandlerFunc { } auth.Error("Internal Server Error").Render(r.Context(), w) - log.Printf("Could not insert user: %v", err) + slog.Error("Could not insert user: %v", err) return } - result := tryCreateSessionAndSetCookie(r, w, db, user_uuid) + result := tryCreateSessionAndSetCookie(r, w, db, userId) if !result { return } w.Header().Add("HX-Redirect", "/") - w.WriteHeader(http.StatusOK) } } -func HandleSignIn(db *sql.DB) http.HandlerFunc { +func HandleSignInComp(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var email = r.FormValue("email") var password = r.FormValue("password") @@ -121,10 +120,10 @@ func HandleSignIn(db *sql.DB) http.HandlerFunc { var result bool = true start := time.Now() - var user_uuid uuid.UUID - var saved_hash []byte + var userId uuid.UUID + var savedHash []byte var salt []byte - err := db.QueryRow("SELECT user_uuid, password, salt FROM user WHERE email = ?", email).Scan(&user_uuid, &saved_hash, &salt) + err := db.QueryRow("SELECT user_uuid, password, salt FROM user WHERE email = ?", email).Scan(&userId, &savedHash, &salt) if err != nil { result = false } @@ -132,26 +131,26 @@ func HandleSignIn(db *sql.DB) http.HandlerFunc { if result { new_hash := getHashPassword(password, salt) - if subtle.ConstantTimeCompare(new_hash, saved_hash) == 0 { + if subtle.ConstantTimeCompare(new_hash, savedHash) == 0 { result = false } } if result { - result := tryCreateSessionAndSetCookie(r, w, db, user_uuid) + result := tryCreateSessionAndSetCookie(r, w, db, userId) if !result { return } } duration := time.Since(start) - time_to_wait := 100 - duration.Milliseconds() + timeToWait := 100 - duration.Milliseconds() // It is important to sleep for a while to prevent timing attacks // If the email is correct, the server will calculate the hash, which will take some time // This way an attacker could guess emails when comparing the response time // Because of that, we cant use WriteHeader in the middle of the function. We have to wait until the end // Unfortunatly this makes the code harder to read - time.Sleep(time.Duration(time_to_wait) * time.Millisecond) + time.Sleep(time.Duration(timeToWait) * time.Millisecond) if result { w.Header().Add("HX-Redirect", "/") @@ -168,7 +167,7 @@ func HandleSignOutComp(db *sql.DB) http.HandlerFunc { _, err := db.Exec("DELETE FROM session WHERE session_id = ?", id) if err != nil { - log.Printf("Could not delete session: %v", err) + slog.Error("Could not delete session: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -188,36 +187,11 @@ func HandleSignOutComp(db *sql.DB) http.HandlerFunc { } } -// var ( -// metricsAuthSignUp = promauto.NewCounterVec( -// prometheus.CounterOpts{ -// Name: "mefit_api_auth_signup_total", -// Help: "The total number of auth signup api requests processed", -// }, -// []string{"result"}, -// ) -// -// metricsError = promauto.NewCounterVec( -// prometheus.CounterOpts{ -// Name: "mefit_api_error_total", -// Help: "The total number of errors", -// }, -// []string{"result"}, -// ) -// -// // metricsAuthSignIn = promauto.NewCounterVec( -// // prometheus.CounterOpts{ -// // Name: "mefit_api_auth_signin_total", -// // }, -// // []string{"result"}, -// // ) -// ) - func tryCreateSessionAndSetCookie(r *http.Request, w http.ResponseWriter, db *sql.DB, user_uuid uuid.UUID) bool { var session_id_bytes []byte = make([]byte, 32) _, err := rand.Reader.Read(session_id_bytes) if err != nil { - log.Printf("Could not generate session ID: %v", err) + slog.Error("Could not generate session ID: %v", err) auth.Error("Internal Server Error").Render(r.Context(), w) return false } @@ -225,7 +199,7 @@ func tryCreateSessionAndSetCookie(r *http.Request, w http.ResponseWriter, db *sq _, err = db.Exec("INSERT INTO session (session_id, user_uuid, created_at) VALUES (?, ?, datetime())", session_id, user_uuid) if err != nil { - log.Printf("Could not insert session: %v", err) + slog.Error("Could not insert session: %v", err) auth.Error("Internal Server Error").Render(r.Context(), w) return false } @@ -254,25 +228,25 @@ func getSessionID(r *http.Request) string { } func verifySessionAndReturnUser(db *sql.DB, r *http.Request) *User { - session_id := getSessionID(r) - if session_id == "" { + sessionId := getSessionID(r) + if sessionId == "" { return nil } var user User - var created_at time.Time + var createdAt time.Time err := db.QueryRow(` SELECT u.user_uuid, u.email, s.created_at FROM session s INNER JOIN user u ON s.user_uuid = u.user_uuid - WHERE session_id = ?`, session_id).Scan(&user.user_uuid, &user.email, &created_at) + WHERE session_id = ?`, sessionId).Scan(&user.id, &user.email, &createdAt) if err != nil { - log.Printf("Could not verify session: %v", err) + slog.Error("Could not verify session: " + err.Error()) return nil } - if created_at.Add(time.Duration(8 * time.Hour)).Before(time.Now()) { + if createdAt.Add(time.Duration(8 * time.Hour)).Before(time.Now()) { return nil } diff --git a/service/static_ui.go b/service/static_ui.go index d86efe8..644c419 100644 --- a/service/static_ui.go +++ b/service/static_ui.go @@ -11,13 +11,13 @@ import ( func HandleIndexAnd404(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var comp templ.Component = nil - user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) + userComp := UserInfoComp(verifySessionAndReturnUser(db, r)) if r.URL.Path != "/" { - comp = template.Layout(template.NotFound(), user_comp) + comp = template.Layout(template.NotFound(), userComp) w.WriteHeader(http.StatusNotFound) } else { - comp = template.Layout(template.Index(), user_comp) + comp = template.Layout(template.Index(), userComp) } comp.Render(r.Context(), w) diff --git a/service/workout.go b/service/workout.go index 97f42a7..e0d5f7b 100644 --- a/service/workout.go +++ b/service/workout.go @@ -8,38 +8,28 @@ import ( "net/http" "strconv" "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - metrics = promauto.NewCounterVec( - prometheus.CounterOpts{ - Name: "mefit_api_workout_total", - Help: "The total number of workout api requests processed", - }, - []string{"type"}, - ) ) func HandleWorkoutPage(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + user := verifySessionAndReturnUser(db, r) + if user == nil { + http.Redirect(w, r, "/auth/signin", http.StatusSeeOther) + return + } + currentDate := time.Now().Format("2006-01-02") inner := workout.WorkoutComp(currentDate) - user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) - layout := template.Layout(inner, user_comp) - layout.Render(r.Context(), w) + userComp := UserInfoComp(user) + template.Layout(inner, userComp).Render(r.Context(), w) } } -func HandleNewWorkout(db *sql.DB) http.HandlerFunc { +func HandleWorkoutNewComp(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - metrics.WithLabelValues("new").Inc() - user := verifySessionAndReturnUser(db, r) if user == nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + w.Header().Add("HX-Redirect", "/auth/signin") return } @@ -71,7 +61,7 @@ func HandleNewWorkout(db *sql.DB) http.HandlerFunc { return } - _, err = db.Exec("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?)", user.user_uuid, date, typeStr, sets, reps) + _, err = db.Exec("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?)", user.id, date, typeStr, sets, reps) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -88,17 +78,15 @@ func HandleNewWorkout(db *sql.DB) http.HandlerFunc { } } -func HandleGetWorkouts(db *sql.DB) http.HandlerFunc { +func HandleWorkoutGetComp(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - metrics.WithLabelValues("get").Inc() - user := verifySessionAndReturnUser(db, r) if user == nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + w.Header().Add("HX-Redirect", "/auth/signin") return } - rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ?", user.user_uuid) + rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ?", user.id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -127,11 +115,13 @@ func HandleGetWorkouts(db *sql.DB) http.HandlerFunc { } } -func HandleDeleteWorkout(db *sql.DB) http.HandlerFunc { +func HandleWorkoutDeleteComp(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - metrics.WithLabelValues("delete").Inc() - user := verifySessionAndReturnUser(db, r) + if user == nil { + w.Header().Add("HX-Redirect", "/auth/signin") + return + } rowId := r.PathValue("id") if rowId == "" { @@ -139,7 +129,7 @@ func HandleDeleteWorkout(db *sql.DB) http.HandlerFunc { return } - res, err := db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", user.user_uuid, rowId) + 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) return diff --git a/template/index.templ b/template/index.templ index 51fd811..9b029a7 100644 --- a/template/index.templ +++ b/template/index.templ @@ -9,7 +9,7 @@ templ Index() { Ever wanted to track your workouts and see your progress over time? ME-FIT is the perfect solution for you.

- Get Started + Get Started diff --git a/utils/db.go b/utils/db.go index f105f88..7e2aa2a 100644 --- a/utils/db.go +++ b/utils/db.go @@ -16,7 +16,7 @@ func RunMigrations(db *sql.DB) { } m, err := migrate.NewWithDatabaseInstance( - "file://./migrations/", + "file://./migration/", "", driver) if err != nil { diff --git a/utils/http-utils.go b/utils/http-utils.go deleted file mode 100644 index 22017ef..0000000 --- a/utils/http-utils.go +++ /dev/null @@ -1,11 +0,0 @@ -package utils - -import ( - "encoding/json" - "net/http" -) - -func WriteJSON(w http.ResponseWriter, data interface{}) error { - w.Header().Set("Content-Type", "application/json") - return json.NewEncoder(w).Encode(data) -}