package cmd import ( "context" "log/slog" "os" "strings" "git.coopcloud.tech/wiki-cafe/member-console/internal/db" "git.coopcloud.tech/wiki-cafe/member-console/internal/logging" "git.coopcloud.tech/wiki-cafe/member-console/internal/server" "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() // Set up structured logging env := viper.GetString("env") logger := logging.SetupLogger(env) // Store logger in context ctx = logging.WithContext(ctx, logger) // Database Setup dbDSN := viper.GetString("db-dsn") dbConfig := db.DefaultDBConfig(dbDSN) database, err := db.ConnectAndMigrate(ctx, logger, dbConfig) if err != nil { logger.Error("failed to initialize database", slog.Any("error", err)) os.Exit(1) } defer database.Close() // You'll pass 'database' (or your sqlc Querier) to your server/handlers. // For example, by adding it to the server.Config // Validate and load configurations from files if specified configPairs := []struct { value string file string configKey string }{ {viper.GetString("oidc-sp-client-secret"), viper.GetString("oidc-sp-client-secret-file"), "oidc-sp-client-secret"}, {viper.GetString("session-secret"), viper.GetString("session-secret-file"), "session-secret"}, {viper.GetString("csrf-secret"), viper.GetString("csrf-secret-file"), "csrf-secret"}, } // Check for conflicts between direct values and file paths for _, pair := range configPairs { // Check if both a direct value and a file path are provided if pair.value != "" && pair.file != "" { logger.Error("configuration error", slog.String("config", pair.configKey), slog.String("error", "both direct value and file path provided; use only one")) return } // If a file path is provided, load the value from the file if pair.file != "" { value, err := loadFromFile(pair.file) if err != nil { logger.Error("failed to load configuration from file", slog.String("config", pair.configKey), slog.String("file", pair.file), slog.Any("error", err)) return } viper.Set(pair.configKey, value) } } // Retrieve the configuration values from Viper port := viper.GetString("port") csrfSecret := viper.GetString("csrf-secret") // Create server config serverConfig := server.Config{ Port: port, Env: env, CSRFSecret: csrfSecret, Logger: logger, DB: db.New(database), // Pass the sqlc Querier } // Start the server if err := server.Start(ctx, serverConfig); err != nil { logger.Error("server failed to start", slog.Any("error", err)) } }, } func init() { // Register flags with Cobra // Do not set default values here. Use viper.SetDefault() instead. https://github.com/spf13/viper/issues/671 startCmd.Flags().StringP("port", "p", "", "Port to listen on") startCmd.Flags().String("oidc-sp-client-id", "", "OIDC Client ID") startCmd.Flags().String("oidc-idp-issuer-url", "", "Identity Provider Issuer URL") startCmd.Flags().String("base-url", "", "Address at which the server is exposed") startCmd.Flags().String("env", "", "Environment (development/production)") startCmd.Flags().String("oidc-sp-client-secret", "", "OIDC Client Secret") startCmd.Flags().String("oidc-sp-client-secret-file", "", "Path to file containing OIDC Client Secret") startCmd.Flags().String("session-secret", "", "Secret key for session management (must be exactly 32 bytes)") startCmd.Flags().String("session-secret-file", "", "Path to file containing session secret key") startCmd.Flags().String("csrf-secret", "", "Secret key for CSRF protection (must be exactly 32 bytes)") startCmd.Flags().String("csrf-secret-file", "", "Path to file containing CSRF secret key") startCmd.Flags().String("db-dsn", "", "Database DSN (e.g., ./member_console.db or file:/path/to/data.db?_foreign_keys=on)") // Bind all flags to Viper viper.BindPFlags(startCmd.Flags()) // Set default values viper.SetDefault("port", "8080") viper.SetDefault("env", "development") viper.SetDefault("db-dsn", "./member_console.db") // Add the command to the root command rootCmd.AddCommand(startCmd) } // loadFromFile reads a file and returns its contents as a trimmed string func loadFromFile(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } return strings.TrimSpace(string(data)), nil }