package route import ( "fmt" "log/slog" "net/http" "runtime/debug" "time" "mkiso-server/internal/config" "mkiso-server/internal/handler" "mkiso-server/internal/middleware" "mkiso-server/internal/service" ) // Setup creates the HTTP handler with all routes wired. // Stage 1: no auth. func Setup(cfg *config.Config, isoSvc *service.ISOService, relaySvc *service.RelayService) http.Handler { mux := http.NewServeMux() // Health check — always public mux.HandleFunc("GET /api/health", handler.Health(cfg)) // ISO download endpoints mux.HandleFunc("GET /api/iso/download", handler.DownloadSingle(isoSvc)) mux.HandleFunc("GET /api/iso/download-multiple", handler.DownloadMultiple(isoSvc)) // Print/relay endpoint (single endpoint handles both single and multi via comma detection) mux.HandleFunc("GET /api/iso/print", handler.PrintISO(relaySvc)) // Wrap with middleware chain: Recovery → RequestID → Logging return middleware.Chain( mux, recoveryMiddleware, requestIDMiddleware, loggingMiddleware, ) } // SetupSecure creates the HTTP handler with API key authentication on /api/iso/*. // Stage 2: auth enabled. func SetupSecure(cfg *config.Config, isoSvc *service.ISOService, relaySvc *service.RelayService) http.Handler { mux := http.NewServeMux() // Health check — always public mux.HandleFunc("GET /api/health", handler.Health(cfg)) // Protected routes apiKeyMw := middleware.APIKey(cfg.Auth.APIKey) mux.Handle("GET /api/iso/download", apiKeyMw(http.HandlerFunc(handler.DownloadSingle(isoSvc)))) mux.Handle("GET /api/iso/download-multiple", apiKeyMw(http.HandlerFunc(handler.DownloadMultiple(isoSvc)))) mux.Handle("GET /api/iso/print", apiKeyMw(http.HandlerFunc(handler.PrintISO(relaySvc)))) return middleware.Chain( mux, recoveryMiddleware, requestIDMiddleware, loggingMiddleware, ) } // recoveryMiddleware catches panics, logs the stack trace, and returns 500. func recoveryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { slog.Error("handler panic", "method", r.Method, "path", r.URL.Path, "panic", rec, "stack", string(debug.Stack()), ) http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } // requestIDMiddleware adds a unique request ID to each request context. func requestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rid := r.Header.Get("X-Request-ID") if rid == "" { rid = generateRequestID() } w.Header().Set("X-Request-ID", rid) next.ServeHTTP(w, r) }) } // loggingMiddleware logs each request with method, path, status, and duration. func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK} next.ServeHTTP(lrw, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "query", r.URL.RawQuery, "status", lrw.statusCode, "duration_ms", time.Since(start).Milliseconds(), ) }) } // loggingResponseWriter wraps http.ResponseWriter to capture the status code. type loggingResponseWriter struct { http.ResponseWriter statusCode int } func (lrw *loggingResponseWriter) WriteHeader(code int) { lrw.statusCode = code lrw.ResponseWriter.WriteHeader(code) } // generateRequestID returns a simple unique request ID. func generateRequestID() string { return fmt.Sprintf("req-%d", time.Now().UnixNano()) }