member-console/cmd/start.go

153 lines
4.9 KiB
Go

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)
}