- шаг схемы закрывает поверхность, которую хранилище приносит открытой: собственную регистрацию, вход по паролю и одноразовый код — без этого закрытие приёма обходилось двумя запросами - продление сессии выключено, срок семь суток: иначе отзыв доступа у провайдера до сервиса не доходит никогда - файл записи отдаётся вошедшему по токену файла — пересмотр ADR-2026-08-12-file-link-open-but-not-logged
158 lines
5.8 KiB
Go
158 lines
5.8 KiB
Go
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
|
|
}
|