initial commit

This commit is contained in:
2026-03-03 18:25:00 +05:00
commit 723d884fc2
16 changed files with 491 additions and 0 deletions

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

@@ -0,0 +1,217 @@
package config
import (
"context"
"fmt"
"log/slog"
"os"
"strings"
"time"
"github.com/joho/godotenv"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)
type Config struct {
Logiflow struct {
LogLevel slog.Level `koanf:"logLevel"`
} `koanf:"logiflow"`
Database struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
User string `koanf:"user"`
Password string `koanf:"password"`
Name string `koanf:"name"`
SslMode string `koanf:"sslmode"`
URL string
} `koanf:"database"`
Server struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
ReadTimeout string `koanf:"readTimeout"`
WriteTimeout string `koanf:"writeTimeout"`
IdleTimeout string `koanf:"idleTimeout"`
ReadTimeoutDur time.Duration
WriteTimeoutDur time.Duration
IdleTimeoutDur time.Duration
URL string
} `koanf:"server"`
Redis struct {
Addr string `koanf:"addr"`
Password string `koanf:"password"`
DB int `koanf:"db"`
RefreshTokenTTL string `koanf:"refreshTokenTTL"`
AccessTokenTTL string `koanf:"accessTokenTTL"`
RefreshTokenDur time.Duration
AccessTokenDur time.Duration
} `koanf:"redis"`
JwtOpt struct {
Key string `koanf:"key"`
Issuer string `koanf:"issuer"`
Audience string `koanf:"audience"`
} `koanf:"jwt"`
}
func NewConfig(ctx context.Context, configPath string) (*Config, error) {
if err := godotenv.Load(); err != nil && !os.IsNotExist(err) {
slog.WarnContext(ctx, "Не удалось загрузить .env (возможно, файла нет)", "error", err)
}
k := koanf.New(".")
if err := k.Load(env.Provider("HANDBOOKS_", ".", func(s string) string {
return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(s, "HANDBOOKS_")), "_", ".")
}), nil); err != nil {
return nil, fmt.Errorf("ошибка загрузки ENV: %w", err)
}
if err := k.Load(file.Provider(configPath), toml.Parser()); err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("не удалось прочитать config.toml: %w", err)
}
slog.InfoContext(ctx, "config.toml не найден — используем только ENV и дефолты")
}
var cfg Config
if err := k.Unmarshal("", &cfg); err != nil {
return nil, fmt.Errorf("не удалось размапить конфигурацию: %w", err)
}
cfg.setDefaults()
if err := cfg.parseDurations(); err != nil {
return nil, fmt.Errorf("ошибка парсинга длительностей: %w", err)
}
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("конфигурация невалидна: %w", err)
}
cfg.Server.URL = fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
cfg.Database.URL = cfg.makePostgresURL()
slog.InfoContext(ctx, "Конфигурация загружена успешно",
slog.String("db_host", cfg.Database.Host),
slog.String("db_user", cfg.Database.User),
slog.String("db_pass", maskSecret(cfg.Database.Password)),
slog.String("db_name", cfg.Database.Name),
slog.String("redis_addr", cfg.Redis.Addr),
slog.String("log_level", cfg.Logiflow.LogLevel.String()),
)
return &cfg, nil
}
func maskSecret(s string) string {
if s == "" {
return "<empty>"
}
if len(s) <= 4 {
return "****"
}
return s[:2] + strings.Repeat("*", len(s)-4) + s[len(s)-2:]
}
func (c *Config) makePostgresURL() string {
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
c.Database.User,
c.Database.Password,
c.Database.Host,
c.Database.Port,
c.Database.Name,
c.Database.SslMode,
)
}
func (c *Config) setDefaults() {
if c.Logiflow.LogLevel == 0 {
c.Logiflow.LogLevel = slog.LevelInfo
}
if c.Database.Host == "" {
c.Database.Host = "localhost"
}
if c.Database.Port == 0 {
c.Database.Port = 5432
}
if c.Database.SslMode == "" {
c.Database.SslMode = "disable"
}
if c.Server.Host == "" {
c.Server.Host = "0.0.0.0"
}
if c.Server.Port == 0 {
c.Server.Port = 3001
}
if c.Redis.Addr == "" {
c.Redis.Addr = "localhost:6379"
}
}
func (c *Config) parseDurations() error {
var err error
parse := func(name, s string) (time.Duration, error) {
d, e := time.ParseDuration(s)
if e != nil {
return 0, fmt.Errorf("%s %q: %w", name, s, e)
}
return d, nil
}
c.Server.ReadTimeoutDur, err = parse("readTimeout", c.Server.ReadTimeout)
if err != nil {
return err
}
c.Server.WriteTimeoutDur, err = parse("writeTimeout", c.Server.WriteTimeout)
if err != nil {
return err
}
c.Server.IdleTimeoutDur, err = parse("idleTimeout", c.Server.IdleTimeout)
if err != nil {
return err
}
c.Redis.RefreshTokenDur, err = parse("refreshTokenTTL", c.Redis.RefreshTokenTTL)
if err != nil {
return err
}
c.Redis.AccessTokenDur, err = parse("accessTokenTTL", c.Redis.AccessTokenTTL)
if err != nil {
return err
}
return nil
}
func (c *Config) validate() error {
if c.Database.Host == "" {
return fmt.Errorf("database.host обязателен")
}
if c.Database.User == "" {
return fmt.Errorf("database.user обязателен")
}
if c.Database.Password == "" {
return fmt.Errorf("database.password обязателен")
}
if c.Database.Name == "" {
return fmt.Errorf("database.name обязателен")
}
if c.JwtOpt.Key == "" {
return fmt.Errorf("jwt.key обязателен")
}
return nil
}
func (c *Config) DatabaseURL() string { return c.Database.URL }
func (c *Config) ServerURL() string { return c.Server.URL }
func (c *Config) ReadTimeout() time.Duration { return c.Server.ReadTimeoutDur }
func (c *Config) WriteTimeout() time.Duration { return c.Server.WriteTimeoutDur }
func (c *Config) IdleTimeout() time.Duration { return c.Server.IdleTimeoutDur }
func (c *Config) RedisAccessTokenDur() time.Duration { return c.Redis.AccessTokenDur }
func (c *Config) RedisRefreshTokenDur() time.Duration { return c.Redis.RefreshTokenDur }