initial commit. Cloned timetracker repository
This commit is contained in:
281
app/config/config.go
Normal file
281
app/config/config.go
Normal file
@@ -0,0 +1,281 @@
|
||||
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(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
c.Host,
|
||||
c.Port,
|
||||
c.User,
|
||||
c.Password,
|
||||
c.Name,
|
||||
c.SSLMode,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user