chore: #123 unify metrics, logs, variable names and structure
This commit is contained in:
@@ -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"]
|
||||
|
||||
|
||||
12
handler.go
12
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))
|
||||
|
||||
9
main.go
9
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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
<a href="/app" class="btn btn-primary">Get Started</a>
|
||||
<a href="/workout" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ func RunMigrations(db *sql.DB) {
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://./migrations/",
|
||||
"file://./migration/",
|
||||
"",
|
||||
driver)
|
||||
if err != nil {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user