package config import ( "fmt" "os" "strconv" "strings" "time" ) type Config struct { Addr string DatabaseURL string MediaDir string AllowedOrigins string AdminEmail string AdminPassword string ShutdownTimeout time.Duration Debug bool SiteURL string WebDir string SSRRenderer string NodeBinary string // Уведомления о заявках на почту. Без SMTP_HOST отключены. SMTPHost string SMTPPort int SMTPUser string SMTPPassword string NotifyFrom string NotifyTo []string // Виртуальная АТС МегаФон: адрес ВАТС (vats970345.megapbx.ru) и ключ REST API. // Без ключа загрузка звонков выключена. MegafonURL string MegafonKey string // АТС МегаФон клиники «Очевидно» (vats363024.megapbx.ru) — свои звонки и свой отчёт по каналам. OchevidnoMegafonURL string OchevidnoMegafonKey string // 1С: расписание врачей и запись. Без адреса и ключа запись через 1С выключена, остаются заявки. OnecURL string OnecKey string OnecAuthHeader string OnecWebhookKey string OnecSyncSecs int // период опроса 1С, по умолчанию 300 OnecLongPoll bool // режим long polling: 1С держит запрос /changes // Битрикс24: входящий вебхук для разовой выгрузки номеров лидов/контактов в «Отчёт по каналам». BitrixWebhookURL string // Новая CRM (dana-medccrm): шлюз создания сделок и режим работы. CrmAPIURL string CrmLeadIntakeSecret string CrmMode string // both (по умолчанию) | crm_only | bitrix_only // SMSC.RU: подтверждение номера звонком (Wait-Call) SMSCLogin string SMSCPassword string // Секрет в адресе обратного вызова SMSC (?token=…). Без него вызов не принимается. SMSCCallbackToken string // TrustedProxies — адреса/подсети прокси, чьим X-Real-IP верим (пусто — только сам сервер). TrustedProxies string // Кабинеты ПроДокторов и СберЗдоровья (скачивание с сервера; пусто = выключено). ProdoctorovCookie string ProdoctorovLPU string SberHealthCookie string SberHealthClinic string } func Load() (Config, error) { loadDotEnv(".env") c := Config{ Addr: env("APP_ADDR", ":8080"), DatabaseURL: os.Getenv("DATABASE_URL"), MediaDir: env("MEDIA_DIR", "./media"), AllowedOrigins: env("ALLOWED_ORIGINS", "*"), AdminEmail: env("ADMIN_EMAIL", ""), AdminPassword: env("ADMIN_PASSWORD", ""), ShutdownTimeout: 15 * time.Second, Debug: envBool("DEBUG", false), SiteURL: env("SITE_URL", ""), WebDir: env("WEB_DIR", "./web/dist"), SSRRenderer: env("SSR_RENDERER", "./web/scripts/render-page.mjs"), NodeBinary: env("NODE_BINARY", "node"), SMTPHost: env("SMTP_HOST", ""), SMTPPort: envInt("SMTP_PORT", 465), SMTPUser: env("SMTP_USER", ""), SMTPPassword: os.Getenv("SMTP_PASSWORD"), NotifyFrom: env("NOTIFY_FROM", os.Getenv("SMTP_USER")), NotifyTo: envList("NOTIFY_TO"), MegafonURL: env("MEGAFON_API_URL", ""), MegafonKey: os.Getenv("MEGAFON_API_KEY"), OchevidnoMegafonURL: env("OCHEVIDNO_MEGAFON_API_URL", ""), OchevidnoMegafonKey: os.Getenv("OCHEVIDNO_MEGAFON_API_KEY"), OnecURL: env("ONEC_API_URL", ""), OnecKey: os.Getenv("ONEC_API_KEY"), OnecAuthHeader: env("ONEC_AUTH_HEADER", "X-Api-Key"), OnecWebhookKey: os.Getenv("ONEC_WEBHOOK_KEY"), OnecSyncSecs: envInt("ONEC_SYNC_SECONDS", 300), OnecLongPoll: os.Getenv("ONEC_LONGPOLL") == "1", BitrixWebhookURL: os.Getenv("BITRIX_WEBHOOK_URL"), CrmAPIURL: env("CRM_API_URL", "https://stagingcrm.dana-medical.ru"), CrmLeadIntakeSecret: os.Getenv("CRM_LEAD_INTAKE_SECRET"), CrmMode: env("CRM_MODE", "both"), SMSCLogin: os.Getenv("SMSC_LOGIN"), SMSCPassword: os.Getenv("SMSC_PASSWORD"), SMSCCallbackToken: os.Getenv("SMSC_CALLBACK_TOKEN"), TrustedProxies: os.Getenv("TRUSTED_PROXIES"), ProdoctorovCookie: os.Getenv("PRODOCTOROV_COOKIE"), ProdoctorovLPU: os.Getenv("PRODOCTOROV_LPU_ID"), SberHealthCookie: os.Getenv("SBERHEALTH_COOKIE"), SberHealthClinic: os.Getenv("SBERHEALTH_CLINIC_ID"), } if c.DatabaseURL == "" { return c, fmt.Errorf("DATABASE_URL is required") } if c.SMTPHost != "" && c.NotifyFrom == "" { return c, fmt.Errorf("NOTIFY_FROM or SMTP_USER is required when SMTP_HOST is set") } return c, nil } func envInt(key string, def int) int { n, err := strconv.Atoi(os.Getenv(key)) if err != nil { return def } return n } // envList читает список через запятую: «a@x.ru, b@x.ru». func envList(key string) []string { var out []string for _, part := range strings.Split(os.Getenv(key), ",") { if part = strings.TrimSpace(part); part != "" { out = append(out, part) } } return out } func env(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func envBool(key string, def bool) bool { v := os.Getenv(key) if v == "" { return def } b, err := strconv.ParseBool(v) if err != nil { return def } return b } // loadDotEnv читает файл .env рядом с сервером: строки «КЛЮЧ=значение», # — комментарий. // Переменные, уже заданные в окружении, не перезаписываются. Файл в git не попадает (.gitignore): // в нём живут секреты — пароль базы, ключ API АТС. func loadDotEnv(path string) { data, err := os.ReadFile(path) if err != nil { return } text := strings.TrimPrefix(string(data), "\ufeff") for _, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } k, v, ok := strings.Cut(line, "=") if !ok { continue } k = strings.TrimSpace(strings.TrimPrefix(k, "export ")) v = strings.TrimSpace(v) if len(v) >= 2 && ((v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'')) { v = v[1 : len(v)-1] } if _, set := os.LookupEnv(k); !set && k != "" { _ = os.Setenv(k, v) } } }