feat: Phase 1 — config, auth, OAuth2 PKCE, CLI scaffold, token store

This commit is contained in:
GoCalGoo
2026-04-01 21:25:49 +02:00
parent 514372fa6b
commit 10db895ada
14 changed files with 977 additions and 29 deletions

204
internal/config/config.go Normal file
View File

@@ -0,0 +1,204 @@
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
}