205 lines
6.1 KiB
Go
205 lines
6.1 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type Config struct {
|
|
App AppConfig `mapstructure:"app"`
|
|
OAuth OAuthConfig `mapstructure:"oauth"`
|
|
Google GoogleConfig `mapstructure:"google"`
|
|
Server ServerConfig `mapstructure:"server"`
|
|
API APIConfig `mapstructure:"api"`
|
|
MCP MCPConfig `mapstructure:"mcp"`
|
|
Security SecurityConfig `mapstructure:"security"`
|
|
Output OutputConfig `mapstructure:"output"`
|
|
}
|
|
|
|
type AppConfig struct {
|
|
Name string `mapstructure:"name"`
|
|
Env string `mapstructure:"env"`
|
|
LogLevel string `mapstructure:"log_level"`
|
|
DataDir string `mapstructure:"data_dir"`
|
|
}
|
|
|
|
type OAuthConfig struct {
|
|
ClientCredentialsFile string `mapstructure:"client_credentials_file"`
|
|
TokenStoreFile string `mapstructure:"token_store_file"`
|
|
DefaultPort int `mapstructure:"default_port"`
|
|
OpenBrowser bool `mapstructure:"open_browser"`
|
|
ManualFallback bool `mapstructure:"manual_fallback"`
|
|
CallbackPath string `mapstructure:"callback_path"`
|
|
}
|
|
|
|
type GoogleConfig struct {
|
|
Scopes []string `mapstructure:"scopes"`
|
|
DefaultCalendarID string `mapstructure:"default_calendar_id"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Bind string `mapstructure:"bind"`
|
|
Port int `mapstructure:"port"`
|
|
ReadTimeout time.Duration `mapstructure:"read_timeout"`
|
|
WriteTimeout time.Duration `mapstructure:"write_timeout"`
|
|
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
|
|
}
|
|
|
|
type APIConfig struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
BasePath string `mapstructure:"base_path"`
|
|
Auth AuthCfg `mapstructure:"auth"`
|
|
}
|
|
|
|
type AuthCfg struct {
|
|
OAuthBearerEnabled bool `mapstructure:"oauth_bearer_enabled"`
|
|
APIKeyEnabled bool `mapstructure:"api_key_enabled"`
|
|
}
|
|
|
|
type MCPConfig struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
Stdio bool `mapstructure:"stdio"`
|
|
HTTP bool `mapstructure:"http"`
|
|
HTTPPath string `mapstructure:"http_path"`
|
|
ProtocolVersion string `mapstructure:"protocol_version"`
|
|
SessionRequired bool `mapstructure:"session_required"`
|
|
}
|
|
|
|
type SecurityConfig struct {
|
|
APIKeysFile string `mapstructure:"api_keys_file"`
|
|
RedactSecretsInLogs bool `mapstructure:"redact_secrets_in_logs"`
|
|
RequireTLSForRemote bool `mapstructure:"require_tls_for_remote_http"`
|
|
}
|
|
|
|
type OutputConfig struct {
|
|
Format string `mapstructure:"format"`
|
|
Timezone string `mapstructure:"timezone"`
|
|
}
|
|
|
|
func Load(cfgFile, profile string) (*Config, error) {
|
|
v := viper.New()
|
|
setDefaults(v)
|
|
|
|
v.SetEnvPrefix("GOCALGOO")
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.AutomaticEnv()
|
|
|
|
if cfgFile != "" {
|
|
v.SetConfigFile(cfgFile)
|
|
} else {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get home dir: %w", err)
|
|
}
|
|
cfgName := "config"
|
|
if profile != "" {
|
|
cfgName = "config." + profile
|
|
}
|
|
v.SetConfigName(cfgName)
|
|
v.SetConfigType("yaml")
|
|
v.AddConfigPath(filepath.Join(home, ".config", "gocalgoo"))
|
|
v.AddConfigPath(".")
|
|
v.AddConfigPath("./configs")
|
|
}
|
|
|
|
if err := v.ReadInConfig(); err != nil {
|
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
|
return nil, fmt.Errorf("read config: %w", err)
|
|
}
|
|
}
|
|
|
|
var cfg Config
|
|
if err := v.Unmarshal(&cfg); err != nil {
|
|
return nil, fmt.Errorf("unmarshal config: %w", err)
|
|
}
|
|
|
|
cfg.OAuth.ClientCredentialsFile = expandHome(cfg.OAuth.ClientCredentialsFile)
|
|
cfg.OAuth.TokenStoreFile = expandHome(cfg.OAuth.TokenStoreFile)
|
|
cfg.Security.APIKeysFile = expandHome(cfg.Security.APIKeysFile)
|
|
cfg.App.DataDir = expandHome(cfg.App.DataDir)
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
func Validate(cfg *Config) error {
|
|
if cfg.OAuth.ClientCredentialsFile == "" {
|
|
return fmt.Errorf("oauth.client_credentials_file is required")
|
|
}
|
|
if cfg.OAuth.TokenStoreFile == "" {
|
|
return fmt.Errorf("oauth.token_store_file is required")
|
|
}
|
|
if len(cfg.Google.Scopes) == 0 {
|
|
return fmt.Errorf("google.scopes must not be empty")
|
|
}
|
|
if cfg.Server.Port < 1 || cfg.Server.Port > 65535 {
|
|
return fmt.Errorf("server.port must be 1-65535")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setDefaults(v *viper.Viper) {
|
|
home, _ := os.UserHomeDir()
|
|
|
|
v.SetDefault("app.name", "GoCalGoo")
|
|
v.SetDefault("app.env", "development")
|
|
v.SetDefault("app.log_level", "info")
|
|
v.SetDefault("app.data_dir", filepath.Join(home, ".local", "share", "gocalgoo"))
|
|
|
|
v.SetDefault("oauth.client_credentials_file", filepath.Join(home, ".config", "gocalgoo", "credentials.json"))
|
|
v.SetDefault("oauth.token_store_file", filepath.Join(home, ".config", "gocalgoo", "tokens.json"))
|
|
v.SetDefault("oauth.default_port", 53682)
|
|
v.SetDefault("oauth.open_browser", true)
|
|
v.SetDefault("oauth.manual_fallback", true)
|
|
v.SetDefault("oauth.callback_path", "/oauth/callback")
|
|
|
|
v.SetDefault("google.scopes", []string{
|
|
"https://www.googleapis.com/auth/calendar",
|
|
"https://www.googleapis.com/auth/contacts",
|
|
})
|
|
v.SetDefault("google.default_calendar_id", "primary")
|
|
|
|
v.SetDefault("server.bind", "127.0.0.1")
|
|
v.SetDefault("server.port", 8080)
|
|
v.SetDefault("server.read_timeout", "15s")
|
|
v.SetDefault("server.write_timeout", "30s")
|
|
v.SetDefault("server.shutdown_timeout", "10s")
|
|
|
|
v.SetDefault("api.enabled", true)
|
|
v.SetDefault("api.base_path", "/api/v1")
|
|
v.SetDefault("api.auth.oauth_bearer_enabled", true)
|
|
v.SetDefault("api.auth.api_key_enabled", true)
|
|
|
|
v.SetDefault("mcp.enabled", true)
|
|
v.SetDefault("mcp.stdio", true)
|
|
v.SetDefault("mcp.http", true)
|
|
v.SetDefault("mcp.http_path", "/mcp")
|
|
v.SetDefault("mcp.protocol_version", "2025-06-18")
|
|
v.SetDefault("mcp.session_required", true)
|
|
|
|
v.SetDefault("security.api_keys_file", filepath.Join(home, ".config", "gocalgoo", "api-keys.yaml"))
|
|
v.SetDefault("security.redact_secrets_in_logs", true)
|
|
v.SetDefault("security.require_tls_for_remote_http", false)
|
|
|
|
v.SetDefault("output.format", "table")
|
|
v.SetDefault("output.timezone", "UTC")
|
|
}
|
|
|
|
func expandHome(path string) string {
|
|
if path == "" {
|
|
return path
|
|
}
|
|
if strings.HasPrefix(path, "~/") {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return path
|
|
}
|
|
return filepath.Join(home, path[2:])
|
|
}
|
|
return path
|
|
}
|