chore: #123 unify metrics, logs, variable names and structure
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 45s

This commit is contained in:
Tim
2024-09-02 22:44:59 +02:00
parent 6dfa09211a
commit 0e535a181a
13 changed files with 84 additions and 114 deletions

View File

@@ -16,7 +16,7 @@ WORKDIR /me-fit
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env 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_go /me-fit/me-fit ./me-fit
COPY --from=builder_node /me-fit/static ./static COPY --from=builder_node /me-fit/static ./static
COPY migrations ./migrations COPY migration ./migration
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/me-fit/me-fit"] ENTRYPOINT ["/me-fit/me-fit"]

View File

@@ -16,15 +16,15 @@ func getHandler(db *sql.DB) http.Handler {
// Serve static files (CSS, JS and images) // Serve static files (CSS, JS and images)
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
router.HandleFunc("/app", service.HandleWorkoutPage(db)) router.HandleFunc("/workout", service.HandleWorkoutPage(db))
router.HandleFunc("POST /api/workout", service.HandleNewWorkout(db)) router.HandleFunc("POST /api/workout", service.HandleWorkoutNewComp(db))
router.HandleFunc("GET /api/workout", service.HandleGetWorkouts(db)) router.HandleFunc("GET /api/workout", service.HandleWorkoutGetComp(db))
router.HandleFunc("DELETE /api/workout/{id}", service.HandleDeleteWorkout(db)) router.HandleFunc("DELETE /api/workout/{id}", service.HandleWorkoutDeleteComp(db))
router.HandleFunc("/auth/signin", service.HandleSignInPage(db)) router.HandleFunc("/auth/signin", service.HandleSignInPage(db))
router.HandleFunc("/auth/signup", service.HandleSignUpPage(db)) router.HandleFunc("/auth/signup", service.HandleSignUpPage(db))
router.HandleFunc("/api/auth/signup", service.HandleSignUp(db)) router.HandleFunc("/api/auth/signup", service.HandleSignUpComp(db))
router.HandleFunc("/api/auth/signin", service.HandleSignIn(db)) router.HandleFunc("/api/auth/signin", service.HandleSignInComp(db))
router.HandleFunc("/api/auth/signout", service.HandleSignOutComp(db)) router.HandleFunc("/api/auth/signout", service.HandleSignOutComp(db))
return middleware.Logging(middleware.EnableCors(router)) return middleware.Logging(middleware.EnableCors(router))

View File

@@ -1,10 +1,11 @@
package main package main
import ( import (
"log"
"me-fit/utils" "me-fit/utils"
"database/sql" "database/sql"
"log" "log/slog"
"net/http" "net/http"
"github.com/joho/godotenv" "github.com/joho/godotenv"
@@ -13,7 +14,7 @@ import (
) )
func main() { func main() {
log.Println("Starting server...") slog.Info("Starting server...")
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
@@ -33,7 +34,7 @@ func main() {
Handler: promhttp.Handler(), Handler: promhttp.Handler(),
} }
go func() { go func() {
log.Println("Starting prometheus server at", prometheusServer.Addr) slog.Info("Starting prometheus server on " + prometheusServer.Addr)
err := prometheusServer.ListenAndServe() err := prometheusServer.ListenAndServe()
if err != nil { if err != nil {
panic(err) panic(err)
@@ -44,7 +45,7 @@ func main() {
Addr: ":8080", Addr: ":8080",
Handler: getHandler(db), Handler: getHandler(db),
} }
log.Println("Starting server at", server.Addr) slog.Info("Starting server on " + server.Addr)
err = server.ListenAndServe() err = server.ListenAndServe()
if err != nil { if err != nil {

View File

@@ -2,6 +2,7 @@ package middleware
import ( import (
"log" "log"
"log/slog"
"net/http" "net/http"
"os" "os"
) )
@@ -11,12 +12,11 @@ func EnableCors(next http.Handler) http.Handler {
if base_url == "" { if base_url == "" {
log.Fatal("BASE_URL is not set") 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) { 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-Origin", base_url)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

View File

@@ -1,9 +1,23 @@
package middleware package middleware
import ( import (
"log" "log/slog"
"net/http" "net/http"
"strconv"
"time" "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 { type WrappedWriter struct {
@@ -25,6 +39,8 @@ func Logging(next http.Handler) http.Handler {
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
} }
next.ServeHTTP(wrapped, r) 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()
}) })
} }

View File

@@ -5,7 +5,7 @@ import (
"crypto/subtle" "crypto/subtle"
"database/sql" "database/sql"
"encoding/base64" "encoding/base64"
"log" "log/slog"
"net/http" "net/http"
"net/mail" "net/mail"
"strings" "strings"
@@ -20,8 +20,8 @@ import (
) )
type User struct { type User struct {
user_uuid uuid.UUID id uuid.UUID
email string email string
} }
func HandleSignInPage(db *sql.DB) http.HandlerFunc { 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) { return func(w http.ResponseWriter, r *http.Request) {
var email = r.FormValue("email") var email = r.FormValue("email")
var password = r.FormValue("password") var password = r.FormValue("password")
@@ -69,9 +69,9 @@ func HandleSignUp(db *sql.DB) http.HandlerFunc {
return return
} }
user_uuid, err := uuid.NewRandom() userId, err := uuid.NewRandom()
if err != nil { 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) auth.Error("Internal Server Error").Render(r.Context(), w)
return return
} }
@@ -79,14 +79,14 @@ func HandleSignUp(db *sql.DB) http.HandlerFunc {
salt := make([]byte, 16) salt := make([]byte, 16)
_, err = rand.Read(salt) _, err = rand.Read(salt)
if err != nil { 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) auth.Error("Internal Server Error").Render(r.Context(), w)
return return
} }
hash := getHashPassword(password, salt) 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 { if err != nil {
// This does leak information about the email being in use, though not specifically stated // 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 // 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) 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 return
} }
result := tryCreateSessionAndSetCookie(r, w, db, user_uuid) result := tryCreateSessionAndSetCookie(r, w, db, userId)
if !result { if !result {
return return
} }
w.Header().Add("HX-Redirect", "/") 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) { return func(w http.ResponseWriter, r *http.Request) {
var email = r.FormValue("email") var email = r.FormValue("email")
var password = r.FormValue("password") var password = r.FormValue("password")
@@ -121,10 +120,10 @@ func HandleSignIn(db *sql.DB) http.HandlerFunc {
var result bool = true var result bool = true
start := time.Now() start := time.Now()
var user_uuid uuid.UUID var userId uuid.UUID
var saved_hash []byte var savedHash []byte
var salt []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 { if err != nil {
result = false result = false
} }
@@ -132,26 +131,26 @@ func HandleSignIn(db *sql.DB) http.HandlerFunc {
if result { if result {
new_hash := getHashPassword(password, salt) new_hash := getHashPassword(password, salt)
if subtle.ConstantTimeCompare(new_hash, saved_hash) == 0 { if subtle.ConstantTimeCompare(new_hash, savedHash) == 0 {
result = false result = false
} }
} }
if result { if result {
result := tryCreateSessionAndSetCookie(r, w, db, user_uuid) result := tryCreateSessionAndSetCookie(r, w, db, userId)
if !result { if !result {
return return
} }
} }
duration := time.Since(start) 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 // 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 // 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 // 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 // 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 // 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 { if result {
w.Header().Add("HX-Redirect", "/") 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) _, err := db.Exec("DELETE FROM session WHERE session_id = ?", id)
if err != nil { 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) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func tryCreateSessionAndSetCookie(r *http.Request, w http.ResponseWriter, db *sql.DB, user_uuid uuid.UUID) bool {
var session_id_bytes []byte = make([]byte, 32) var session_id_bytes []byte = make([]byte, 32)
_, err := rand.Reader.Read(session_id_bytes) _, err := rand.Reader.Read(session_id_bytes)
if err != nil { 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) auth.Error("Internal Server Error").Render(r.Context(), w)
return false 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) _, err = db.Exec("INSERT INTO session (session_id, user_uuid, created_at) VALUES (?, ?, datetime())", session_id, user_uuid)
if err != nil { 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) auth.Error("Internal Server Error").Render(r.Context(), w)
return false return false
} }
@@ -254,25 +228,25 @@ func getSessionID(r *http.Request) string {
} }
func verifySessionAndReturnUser(db *sql.DB, r *http.Request) *User { func verifySessionAndReturnUser(db *sql.DB, r *http.Request) *User {
session_id := getSessionID(r) sessionId := getSessionID(r)
if session_id == "" { if sessionId == "" {
return nil return nil
} }
var user User var user User
var created_at time.Time var createdAt time.Time
err := db.QueryRow(` err := db.QueryRow(`
SELECT u.user_uuid, u.email, s.created_at SELECT u.user_uuid, u.email, s.created_at
FROM session s FROM session s
INNER JOIN user u ON s.user_uuid = u.user_uuid 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 { if err != nil {
log.Printf("Could not verify session: %v", err) slog.Error("Could not verify session: %v", err)
return nil 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 return nil
} }

View File

@@ -11,13 +11,13 @@ import (
func HandleIndexAnd404(db *sql.DB) http.HandlerFunc { func HandleIndexAnd404(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var comp templ.Component = nil var comp templ.Component = nil
user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) userComp := UserInfoComp(verifySessionAndReturnUser(db, r))
if r.URL.Path != "/" { if r.URL.Path != "/" {
comp = template.Layout(template.NotFound(), user_comp) comp = template.Layout(template.NotFound(), userComp)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
} else { } else {
comp = template.Layout(template.Index(), user_comp) comp = template.Layout(template.Index(), userComp)
} }
comp.Render(r.Context(), w) comp.Render(r.Context(), w)

View File

@@ -8,38 +8,28 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"time" "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 { func HandleWorkoutPage(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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") currentDate := time.Now().Format("2006-01-02")
inner := workout.WorkoutComp(currentDate) inner := workout.WorkoutComp(currentDate)
user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) userComp := UserInfoComp(user)
layout := template.Layout(inner, user_comp) template.Layout(inner, userComp).Render(r.Context(), w)
layout.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) { return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("new").Inc()
user := verifySessionAndReturnUser(db, r) user := verifySessionAndReturnUser(db, r)
if user == nil { if user == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized) w.Header().Add("HX-Redirect", "/auth/signin")
return return
} }
@@ -71,7 +61,7 @@ func HandleNewWorkout(db *sql.DB) http.HandlerFunc {
return 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 { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) 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) { return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("get").Inc()
user := verifySessionAndReturnUser(db, r) user := verifySessionAndReturnUser(db, r)
if user == nil { if user == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized) w.Header().Add("HX-Redirect", "/auth/signin")
return 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 { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) { return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("delete").Inc()
user := verifySessionAndReturnUser(db, r) user := verifySessionAndReturnUser(db, r)
if user == nil {
w.Header().Add("HX-Redirect", "/auth/signin")
return
}
rowId := r.PathValue("id") rowId := r.PathValue("id")
if rowId == "" { if rowId == "" {
@@ -139,7 +129,7 @@ func HandleDeleteWorkout(db *sql.DB) http.HandlerFunc {
return 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 { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@@ -9,7 +9,7 @@ templ Index() {
Ever wanted to track your workouts and see your progress over time? ME-FIT is the perfect Ever wanted to track your workouts and see your progress over time? ME-FIT is the perfect
solution for you. solution for you.
</p> </p>
<a href="/app" class="btn btn-primary">Get Started</a> <a href="/workout" class="btn btn-primary">Get Started</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,7 +16,7 @@ func RunMigrations(db *sql.DB) {
} }
m, err := migrate.NewWithDatabaseInstance( m, err := migrate.NewWithDatabaseInstance(
"file://./migrations/", "file://./migration/",
"", "",
driver) driver)
if err != nil { if err != nil {

View File

@@ -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)
}