diff --git a/backend/main.go b/backend/main.go index 6cb6c7c..dfa29af 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,6 +4,8 @@ import ( "context" "crussell/auth" "crussell/internal/dav" + "crussell/internal/jobs" + "crussell/internal/logutil" "crussell/internal/s3" "crussell/internal/square" "encoding/json" @@ -13,6 +15,7 @@ import ( "os" "os/signal" "strings" + "sync/atomic" "syscall" "time" @@ -65,13 +68,35 @@ func limitBody(limit int64) func(http.Handler) http.Handler { } const ( - defaultBodyLimit int64 = 1 * 1024 * 1024 // 1MB - uploadBodyLimit int64 = 15 * 1024 * 1024 // 15MB - portfolioBodyLimit int64 = 40 * 1024 * 1024 // 40MB (7 variants from 20MB source) - reservationCleanupInterval = 5 * time.Minute - reservationCleanupTimeout = 30 * time.Second + defaultBodyLimit int64 = 1 * 1024 * 1024 // 1MB + uploadBodyLimit int64 = 15 * 1024 * 1024 // 15MB + portfolioBodyLimit int64 = 40 * 1024 * 1024 // 40MB (7 variants from 20MB source) ) +// nColor / bColor — Chi-style ANSI colors for request logging. +type nColor string +type bColor string + +var ( + reset = nColor(logutil.Reset) + nGreen = nColor(logutil.Green) + nYellow = nColor(logutil.Yellow) + nCyan = nColor(logutil.Cyan) + nRed = nColor(logutil.Red) + + bGreen = bColor(logutil.BoldGreen) + bYellow = bColor(logutil.BoldYellow) + bRed = bColor(logutil.BoldRed) + bBlue = bColor(logutil.BoldBlue) + bMagenta = bColor(logutil.BoldMagenta) + + debugLvl = nColor(logutil.DebugLvl) + warnLvl = nColor(logutil.WarnLvl) + errorLvl = nColor(logutil.ErrorLvl) +) + +var apiLog = log.New(os.Stdout, "", log.LstdFlags) + func initDB() { if err := db.Connect(); err != nil { log.Fatal("Failed to connect to DB:", err) @@ -139,14 +164,63 @@ func main() { initDav() initS3() initSquare() - auth.StartJTICleanup() + + sched := jobs.New() + jobs.RegisterAll(sched) + sched.Start() r := chi.NewRouter() // --- Global Middleware --- - r.Use(middleware.RequestID) + // Custom RequestID using shared random prefix (matches jobs scheduler) + var reqCounter atomic.Uint64 + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + myid := reqCounter.Add(1) + requestID := fmt.Sprintf("%s/%s-%06d", jobs.Hostname(), jobs.RandomPrefix(), myid) + ctx := context.WithValue(r.Context(), middleware.RequestIDKey, requestID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + }) r.Use(middleware.ClientIPFromHeader("X-Real-IP")) - r.Use(middleware.Logger) + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + t1 := time.Now() + defer func() { + status := ww.Status() + bytes := ww.BytesWritten() + reqID := middleware.GetReqID(r.Context()) + + var level nColor + var statusColor bColor + switch { + case status >= 500: + level, statusColor = errorLvl, bRed + case status >= 400: + level, statusColor = warnLvl, bYellow + default: + level, statusColor = debugLvl, bGreen + } + + clientIP := middleware.GetClientIP(r.Context()) + if clientIP == "" { + clientIP = r.RemoteAddr + } + apiLog.Printf("%s %s%s%s \"%s%s %s%s %s%s\" from %s - %s %s%03d%s %s%dB%s in %s", + level, + nYellow, reqID, reset, + bMagenta, r.Method, nCyan, r.URL.String(), nCyan, r.Proto, reset, + clientIP, + statusColor, status, reset, + bBlue, bytes, reset, + logutil.ColoredDuration(time.Since(t1)), + ) + }() + next.ServeHTTP(ww, r) + }) + }) + r.Use(middleware.Recoverer) r.Use(middleware.Timeout(15 * time.Second)) r.Use(func(next http.Handler) http.Handler { @@ -429,26 +503,6 @@ r.Route("/admin/users", func(r chi.Router) { // Webhooks (no auth - Square sends to base path) r.Post("/webhooks/square", webhooks.HandleSquareWebhook) - // Background cleanup of expired reservations - cleanupCtx, cleanupStop := context.WithCancel(context.Background()) - go func() { - ticker := time.NewTicker(reservationCleanupInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - ctx, cancel := context.WithTimeout(context.Background(), reservationCleanupTimeout) - if err := scheduling.CleanupOldReservations(ctx); err != nil { - log.Printf("reservation cleanup error: %v", err) - } - cancel() - case <-cleanupCtx.Done(): - log.Println("reservation cleanup goroutine stopped") - return - } - } - }() - srv := &http.Server{ Addr: ":8080", Handler: r, @@ -464,7 +518,8 @@ r.Route("/admin/users", func(r chi.Router) { if err := srv.Shutdown(ctx); err != nil { log.Printf("Server forced to shutdown: %v", err) } - cleanupStop() + <-sched.Shutdown() + log.Println("Background jobs stopped") }() fmt.Println("Server is listening on :8080")