package server import ( "context" "html/template" "io/fs" "log/slog" "net" "net/http" "time" "git.coopcloud.tech/wiki-cafe/member-console/internal/auth" "git.coopcloud.tech/wiki-cafe/member-console/internal/embeds" "git.coopcloud.tech/wiki-cafe/member-console/internal/middleware" "github.com/rs/cors" "github.com/spf13/viper" ) // 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 } // For embedded templates templateSubFS, err := fs.Sub(embeds.Templates, "templates") if err != nil { cfg.Logger.Error("Failed to create sub filesystem for templates", slog.Any("error", err)) return err } // Parse templates from embedded FS tmpl := template.Must(template.ParseFS(templateSubFS, "*.html")) // Serve index.html via template rendering httpRequestRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Always serve HTML w.Header().Set("Content-Type", "text/html; charset=utf-8") // Ensure user is authenticated session, err := authConfig.Store.Get(r, authConfig.SessionName) if err != nil { http.Redirect(w, r, "/login", http.StatusFound) return } name, _ := session.Values["name"].(string) username, _ := session.Values["username"].(string) email, _ := session.Values["email"].(string) // Create Keycloak Account URL keycloakAccountURL := viper.GetString("oidc-idp-issuer-url") + "/account" data := struct { Name string Username string Email string KeycloakAccountURL string }{Name: name, Username: username, Email: email, KeycloakAccountURL: keycloakAccountURL} if err := tmpl.ExecuteTemplate(w, "index.html", data); err != nil { cfg.Logger.Error("template execute error", slog.Any("error", err)) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }) // For embedded static files httpRequestRouter.Handle("/static/", http.FileServer(http.FS(embeds.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 }