Files
b2b/app/config/config.go
2026-03-16 08:51:35 +01:00

294 lines
6.7 KiB
Go

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
}