package cmd import ( "context" "crypto/rand" "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" "github.com/spf13/cobra" "github.com/spf13/viper" ) var startCmd = &cobra.Command{ Use: "start", Short: "Start serving the member-console web application", Long: `The start command starts an HTTP server that serves the member-console web application from the components directory in the current directory. The server listens on port 8080 by default, unless a different port is specified using the --port flag.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Create base context for the application ctx := context.Background() // Retrieve the configuration values from Viper port := viper.GetString("port") env := viper.GetString("env") // Set up structured logging logger := logging.SetupLogger(env) // 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 } // 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 // Set CSRF secret from config or generate a random one csrfSecret := viper.GetString("csrf-secret") var csrfKey []byte if csrfSecret != "" { // Use configured secret - must be at least 32 bytes csrfKey = []byte(csrfSecret) if len(csrfKey) < 32 { logger.Error("csrf-secret must be at least 32 bytes") return } } else { // Generate a random secret csrfKey = make([]byte, 32) _, err = rand.Read(csrfKey) if err != nil { logger.Error("failed to generate CSRF key", slog.Any("error", err)) return } logger.Info("generated random CSRF key, consider setting csrf-secret for stability across restarts") } 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 authConfig.Middleware(), // OIDC authentication middleware ) // Create HTTP server server := http.Server{ Addr: ":" + port, Handler: stack(httpRequestRouter), ReadTimeout: 2 * time.Second, WriteTimeout: 4 * time.Second, IdleTimeout: 8 * time.Second, MaxHeaderBytes: 1024 * 1024, // 1MB BaseContext: func(_ net.Listener) context.Context { return ctx }, // Pass base context to all requests } // Serve the components directory httpRequestRouter.Handle("/", http.FileServer(http.Dir("./components"))) // 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)) } }, } func init() { // Register flags with Cobra startCmd.Flags().StringP("port", "p", "", "Port to listen on") startCmd.Flags().String("client-id", "", "OIDC Client ID") startCmd.Flags().String("client-secret", "", "OIDC Client Secret") startCmd.Flags().String("issuer-url", "", "Identity Provider Issuer URL") startCmd.Flags().String("hostname", "", "Address at which the server is exposed") startCmd.Flags().String("session-secret", "", "Session encryption secret") startCmd.Flags().String("csrf-secret", "", "Secret key for CSRF protection (min 32 bytes)") startCmd.Flags().String("env", "", "Environment (development/production)") // Bind all flags to Viper viper.BindPFlags(startCmd.Flags()) // Set default values viper.SetDefault("port", "8080") viper.SetDefault("env", "development") // Add the command to the root command rootCmd.AddCommand(startCmd) }