280 lines
6.1 KiB
Go
280 lines
6.1 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
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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(), "")
|
|
}
|
|
|
|
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
|
|
}
|