package config import ( "fmt" "log/slog" "os" "reflect" "strconv" "strings" "time" "github.com/joho/godotenv" ) type Config struct { Server ServerConfig Database DatabaseConfig Auth AuthConfig OAuth OAuthConfig App AppConfig Email EmailConfig I18n I18n Pdf PdfPrinter GoogleTranslate GoogleTranslateConfig } type I18n struct { Langs []string `env:"I18N_LANGS,en,pl"` } type ServerConfig struct { Port int `env:"SERVER_PORT,3000"` Host string `env:"SERVER_HOST,0.0.0.0"` } type DatabaseConfig struct { Host string `env:"DB_HOST,localhost"` Port int `env:"DB_PORT"` User string `env:"DB_USER"` Password string `env:"DB_PASSWORD"` Name string `env:"DB_NAME"` SSLMode string `env:",disable"` MaxIdleConns int `env:",10"` MaxOpenConns int `env:",100"` ConnMaxLifetime time.Duration `env:",1h"` } type AuthConfig struct { JWTSecret string `env:"AUTH_JWT_SECRET"` JWTExpiration int `env:"AUTH_JWT_EXPIRATION"` RefreshExpiration int `env:"AUTH_REFRESH_EXPIRATION"` } type OAuthConfig struct { Google GoogleOAuthConfig } type GoogleOAuthConfig struct { ClientID string `env:"OAUTH_GOOGLE_CLIENT_ID"` ClientSecret string `env:"OAUTH_GOOGLE_CLIENT_SECRET"` RedirectURL string `env:"OAUTH_GOOGLE_REDIRECT_URL"` Scopes []string `env:""` } type AppConfig struct { Name string `env:"APP_NAME,Gitea Manager"` Version string `env:"APP_VERSION,1.0.0"` Environment string `env:"APP_ENVIRONMENT,development"` BaseURL string `env:"APP_BASE_URL,http://localhost:5173"` } type EmailConfig struct { SMTPHost string `env:"EMAIL_SMTP_HOST,localhost"` SMTPPort int `env:"EMAIL_SMTP_PORT,587"` SMTPUser string `env:"EMAIL_SMTP_USER"` SMTPPassword string `env:"EMAIL_SMTP_PASSWORD"` FromEmail string `env:"EMAIL_FROM,noreply@example.com"` FromName string `env:"EMAIL_FROM_NAME,Gitea Manager"` AdminEmail string `env:"EMAIL_ADMIN,admin@example.com"` Enabled bool `env:"EMAIL_ENABLED,false"` } type PdfPrinter struct { ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"` } // GoogleTranslateConfig holds configuration for the Google Cloud Translation API. // CredentialsFile should point to a service account JSON key file. // ProjectID is your Google Cloud project ID (e.g. "my-project-123"). type GoogleTranslateConfig struct { CredentialsFile string `env:"GOOGLE_APPLICATION_CREDENTIALS"` ProjectID string `env:"GOOGLE_CLOUD_PROJECT_ID"` } var cfg *Config func init() { if cfg == nil { cfg = load() } } func Get() *Config { return cfg } // GetDSN returns the database connection string func (c *DatabaseConfig) GetDSN() string { return fmt.Sprintf( "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.User, c.Password, c.Host, c.Port, c.Name, ) } func load() *Config { cfg := &Config{} err := godotenv.Load(".env") if err != nil { slog.Error("not possible to load env: %s", err.Error(), "") } err = loadEnv(&cfg.Database) if err != nil { slog.Error("not possible to load env variables for database : ", err.Error(), "") } err = loadEnv(&cfg.Server) if err != nil { slog.Error("not possible to load env variables for server : ", err.Error(), "") } err = loadEnv(&cfg.Auth) if err != nil { slog.Error("not possible to load env variables for auth : ", err.Error(), "") } err = loadEnv(&cfg.OAuth.Google) if err != nil { slog.Error("not possible to load env variables for outh google : ", err.Error(), "") } err = loadEnv(&cfg.App) if err != nil { slog.Error("not possible to load env variables for app : ", err.Error(), "") } err = loadEnv(&cfg.Email) if err != nil { slog.Error("not possible to load env variables for email : ", err.Error(), "") } err = loadEnv(&cfg.I18n) if err != nil { slog.Error("not possible to load env variables for email : ", err.Error(), "") } err = loadEnv(&cfg.Pdf) if err != nil { slog.Error("not possible to load env variables for email : ", err.Error(), "") } err = loadEnv(&cfg.GoogleTranslate) if err != nil { slog.Error("not possible to load env variables for google translate : ", err.Error(), "") } return cfg } func loadEnv(dst any) error { v := reflect.ValueOf(dst) if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct { return fmt.Errorf("dst must be pointer to struct") } return loadStruct(v.Elem()) } func loadStruct(v reflect.Value) error { t := v.Type() for i := 0; i < v.NumField(); i++ { field := v.Field(i) fieldType := t.Field(i) if !field.CanSet() { continue } // nested struct if field.Kind() == reflect.Struct && field.Type() != reflect.TypeOf(time.Duration(0)) { if err := loadStruct(field); err != nil { return err } continue } tag := fieldType.Tag.Get("env") key, def := parseEnvTag(tag) if key == "" { key = fieldType.Name } val, ok := os.LookupEnv(key) // fallback to default if !ok && def != nil { val = *def ok = true } if !ok { continue } if err := setValue(field, val, key); err != nil { return err } } return nil } func setValue(field reflect.Value, val string, key string) error { switch field.Kind() { case reflect.String: field.SetString(val) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // time.Duration if field.Type() == reflect.TypeOf(time.Duration(0)) { d, err := time.ParseDuration(val) if err != nil { return fmt.Errorf("env %s: %w", key, err) } field.SetInt(int64(d)) return nil } i, err := strconv.Atoi(val) if err != nil { return fmt.Errorf("env %s: %w", key, err) } field.SetInt(int64(i)) case reflect.Bool: b, err := strconv.ParseBool(val) if err != nil { return fmt.Errorf("env %s: %w", key, err) } field.SetBool(b) case reflect.Slice: if field.Type().Elem().Kind() == reflect.String { // Split by comma and trim whitespace parts := strings.Split(val, ",") slice := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { slice = append(slice, p) } } field.Set(reflect.ValueOf(slice)) } else { return fmt.Errorf("unsupported slice type %s for env %s", field.Type().Elem().Kind(), key) } default: return fmt.Errorf("unsupported type %s for env %s", field.Kind(), key) } return nil } func parseEnvTag(tag string) (key string, def *string) { if tag == "" { return "", nil } parts := strings.SplitN(tag, ",", 2) key = parts[0] if len(parts) == 2 { return key, &parts[1] // Returns "en,pl,de" for slices - setValue handles the split } return key, nil }