package config import ( "fmt" "net/url" "os" "sort" "strings" "github.com/BurntSushi/toml" ) type Config struct { Server ServerConfig `toml:"server"` Storage StorageConfig `toml:"storage"` Yandex YandexConfig `toml:"yandex"` Telegram TelegramConfig `toml:"telegram"` Auth AuthConfig `toml:"auth"` } type ServerConfig struct { Port int `toml:"port"` ShutdownTimeout int `toml:"shutdown_timeout"` ForceShutdownTimeout int `toml:"force_shutdown_timeout"` UsersWhiteList []string `toml:"users_while_list"` } // StorageConfig — единственный каталог данных: под ним лежат и база, и файлы // записей. Двух путей, как было раньше, у хранилища не бывает. type StorageConfig struct { DataDir string `toml:"data_dir"` } type YandexConfig struct { FolderID string `toml:"folder_id"` SpeechKitAPIKey string `toml:"speech_kit_api_key"` ObjStorageAccessKey string `toml:"object_storage_access_key_id"` ObjStorageSecretKey string `toml:"object_storage_secret_access_key"` ObjStorageBucketName string `toml:"object_storage_bucket_name"` ObjStorageRegion string `toml:"object_storage_region"` ObjStorageEndpoint string `toml:"object_storage_endpoint"` } type TelegramConfig struct { BotToken string `toml:"bot_token"` UpdateTimeout int `toml:"update_timeout"` } // AuthConfig — вход через внешнего провайдера OIDC. Адреса, идентификатор // клиента и секрет приезжают сюда и приводятся к настройкам коллекции // пользователей при каждом подъёме: применённый шаг схемы не переписывается, и // секрет, положенный однажды шагом, не пережил бы ротации. type AuthConfig struct { AuthURL string `toml:"auth_url"` TokenURL string `toml:"token_url"` UserInfoURL string `toml:"user_info_url"` ClientID string `toml:"client_id"` ClientSecret string `toml:"client_secret"` // RedirectURL — адрес возврата, тот же, что записан клиенту у провайдера. RedirectURL string `toml:"redirect_url"` // SecureCookie — признак `Secure` у куки сессии. Умолчание «включено»; // выключается только для локального запуска по `http://localhost`, где // браузер такую куку не сохранит. SecureCookie bool `toml:"secure_cookie"` } // Validate проверяет, что вход настроен целиком и что адреса — адреса. Пустое // или негодное поле роняет старт: с молча выключенным входом сервис поднялся бы // открытым наружу, а узнать об этом было бы неоткуда. // // Форма адреса проверяется здесь, а не только хранилищем, потому что хранилище // отвергает негодный адрес позже — из хука подъёма, до регистрации пробы // здоровья и метрик. Тогда владелец не получает даже кода состояния: сервис // молча падает целиком, вместе с ботом и воркерами. func (c AuthConfig) Validate() error { values := map[string]string{ "auth_url": c.AuthURL, "token_url": c.TokenURL, "user_info_url": c.UserInfoURL, "client_id": c.ClientID, "client_secret": c.ClientSecret, "redirect_url": c.RedirectURL, } missing := make([]string, 0, len(values)) for name, value := range values { if value == "" { missing = append(missing, name) } } if len(missing) > 0 { sort.Strings(missing) // Названы имена ключей, а не значения: значение `client_secret` в // сообщение об ошибке попасть не должно, оно уедет в журнал. return fmt.Errorf("auth: не заполнены ключи: %s", strings.Join(missing, ", ")) } malformed := make([]string, 0, 4) for _, name := range []string{"auth_url", "token_url", "user_info_url", "redirect_url"} { parsed, err := url.Parse(values[name]) if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { malformed = append(malformed, name) } } if len(malformed) > 0 { return fmt.Errorf("auth: ключи не похожи на адрес: %s", strings.Join(malformed, ", ")) } return nil } // DefaultConfig returns a Config with default values func defaultConfig() *Config { return &Config{ Server: ServerConfig{ Port: 8080, ShutdownTimeout: 5, ForceShutdownTimeout: 20, }, Storage: StorageConfig{ DataDir: "data", }, Yandex: YandexConfig{ FolderID: "", SpeechKitAPIKey: "", ObjStorageAccessKey: "", ObjStorageSecretKey: "", ObjStorageBucketName: "", ObjStorageRegion: "ru-central1", ObjStorageEndpoint: "https://storage.yandexcloud.net/", }, Telegram: TelegramConfig{ BotToken: "", UpdateTimeout: 10, }, Auth: AuthConfig{ SecureCookie: true, }, } } // LoadConfig loads configuration from a TOML file func LoadConfig(path string) (*Config, error) { // Check if file exists if _, err := os.Stat(path); os.IsNotExist(err) { return nil, fmt.Errorf("config file not found: %s", path) } config := defaultConfig() // Load configuration from file if _, err := toml.DecodeFile(path, &config); err != nil { return nil, fmt.Errorf("failed to decode config file: %w", err) } return config, nil }