diff --git a/cmd/start.go b/cmd/start.go index 23f3433..ea85a98 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -3,14 +3,9 @@ package cmd import ( "context" "log/slog" - "net" - "net/http" - "time" - "git.coopcloud.tech/wiki-cafe/member-console/internal/auth" "git.coopcloud.tech/wiki-cafe/member-console/internal/logging" - "git.coopcloud.tech/wiki-cafe/member-console/internal/middleware" - "github.com/rs/cors" + "git.coopcloud.tech/wiki-cafe/member-console/internal/server" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -30,6 +25,7 @@ var startCmd = &cobra.Command{ // Retrieve the configuration values from Viper port := viper.GetString("port") env := viper.GetString("env") + csrfSecret := viper.GetString("csrf-secret") // Set up structured logging logger := logging.SetupLogger(env) @@ -37,86 +33,17 @@ var startCmd = &cobra.Command{ // Store logger in context ctx = logging.WithContext(ctx, logger) - // Create a new HTTP request router - httpRequestRouter := http.NewServeMux() - - // Set up authentication - authConfig, err := auth.Setup() - if err != nil { - logger.Error("failed to set up authentication", slog.Any("error", err)) - return + // Create server config + serverConfig := server.Config{ + Port: port, + Env: env, + CSRFSecret: csrfSecret, + Logger: logger, } - // Register auth handlers - authConfig.RegisterHandlers(httpRequestRouter) - - // Create CORS configuration with default options - corsOptions := cors.Options{ - // Define minimal defaults - GET method is required - AllowedMethods: []string{"GET"}, - } - - // Create empty CSRF configuration with default values - var csrfConfig middleware.CSRFConfig - - // Get and validate CSRF secret from config - csrfSecret := viper.GetString("csrf-secret") - csrfKey, err := middleware.ParseCSRFKey(csrfSecret) - if err != nil { - logger.Error("invalid csrf-secret", - slog.String("error", err.Error()), - slog.String("hint", "must be exactly 32 bytes and persist across restarts")) - return - } - - csrfConfig.Secret = csrfKey - - // Only override specific settings when needed - if env == "development" { - // In development, cookies often need to work without HTTPS - csrfConfig.Cookie.Secure = false - } - - // Create middleware stack - stack := middleware.CreateStack( - middleware.RequestID(), // Generate a unique request ID - middleware.Logging(), // Log requests with structured logging - middleware.Recovery(), // Catch all panics - middleware.Timeout(32*time.Second), // Set request timeout - middleware.MaxBodySize(1024*1024), // 1MB size limit - middleware.SecureHeaders(), // Set secure headers - middleware.CORS(corsOptions), // CORS configuration - middleware.CSRF(csrfConfig), // CSRF protection - middleware.Compress(), // Response compression - authConfig.Middleware(), // OIDC authentication middleware - ) - - // Create HTTP server - server := http.Server{ - Addr: ":" + port, - Handler: stack(httpRequestRouter), - ReadTimeout: 4 * time.Second, - WriteTimeout: 8 * time.Second, - IdleTimeout: 16 * time.Second, - MaxHeaderBytes: 1024 * 1024, // 1MB - BaseContext: func(_ net.Listener) context.Context { return ctx }, // Pass base context to all requests - } - - // Serve the templates and static files - // Serve templates from the "templates" directory - httpRequestRouter.Handle("/", http.FileServer(http.Dir("./templates"))) - // Serve static files from the "static" directory - httpRequestRouter.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) - - // Log server startup with structured logging - logger.Info("starting server", - slog.String("port", port), - slog.String("environment", env), - slog.String("address", "http://localhost:"+port)) - - // Start server and log any errors - if err := server.ListenAndServe(); err != nil { - logger.Error("server error", slog.Any("error", err)) + // Start the server + if err := server.Start(ctx, serverConfig); err != nil { + logger.Error("server failed to start", slog.Any("error", err)) } }, } diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..43bd776 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,107 @@ +package server + +import ( + "context" + "log/slog" + "net" + "net/http" + "time" + + "git.coopcloud.tech/wiki-cafe/member-console/internal/auth" + "git.coopcloud.tech/wiki-cafe/member-console/internal/middleware" + "github.com/rs/cors" +) + +// Config holds the configuration for the server. +type Config struct { + Port string + Env string + CSRFSecret string + Logger *slog.Logger +} + +// Start initializes and starts the HTTP server. +func Start(ctx context.Context, cfg Config) error { + // Create a new HTTP request router + httpRequestRouter := http.NewServeMux() + + // Set up authentication + authConfig, err := auth.Setup() + if err != nil { + cfg.Logger.Error("failed to set up authentication", slog.Any("error", err)) + return err + } + + // Register auth handlers + authConfig.RegisterHandlers(httpRequestRouter) + + // Create CORS configuration with default options + corsOptions := cors.Options{ + // Define minimal defaults - GET method is required + AllowedMethods: []string{"GET"}, + } + + // Create empty CSRF configuration with default values + var csrfConfig middleware.CSRFConfig + + // Get and validate CSRF secret from config + csrfKey, err := middleware.ParseCSRFKey(cfg.CSRFSecret) + if err != nil { + cfg.Logger.Error("invalid csrf-secret", + slog.String("error", err.Error()), + slog.String("hint", "must be exactly 32 bytes and persist across restarts")) + return err + } + + csrfConfig.Secret = csrfKey + + // Only override specific settings when needed + if cfg.Env == "development" { + // In development, cookies often need to work without HTTPS + csrfConfig.Cookie.Secure = false + } + + // Create middleware stack + stack := middleware.CreateStack( + middleware.RequestID(), // Generate a unique request ID + middleware.Logging(), // Log requests with structured logging + middleware.Recovery(), // Catch all panics + middleware.Timeout(32*time.Second), // Set request timeout + middleware.MaxBodySize(1024*1024), // 1MB size limit + middleware.SecureHeaders(), // Set secure headers + middleware.CORS(corsOptions), // CORS configuration + middleware.CSRF(csrfConfig), // CSRF protection + middleware.Compress(), // Response compression + authConfig.Middleware(), // OIDC authentication middleware + ) + + // Create HTTP server + server := http.Server{ + Addr: ":" + cfg.Port, + Handler: stack(httpRequestRouter), + ReadTimeout: 4 * time.Second, + WriteTimeout: 8 * time.Second, + IdleTimeout: 16 * time.Second, + MaxHeaderBytes: 1024 * 1024, // 1MB + BaseContext: func(_ net.Listener) context.Context { return ctx }, // Pass base context to all requests + } + + // Serve the templates and static files + // Serve templates from the "templates" directory + httpRequestRouter.Handle("/", http.FileServer(http.Dir("./templates"))) + // Serve static files from the "static" directory + httpRequestRouter.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) + + // Log server startup with structured logging + cfg.Logger.Info("starting server", + slog.String("port", cfg.Port), + slog.String("environment", cfg.Env), + slog.String("address", "http://localhost:"+cfg.Port)) + + // Start server and log any errors + if err := server.ListenAndServe(); err != nil { + cfg.Logger.Error("server error", slog.Any("error", err)) + return err + } + return nil +}