package app import ( "fmt" "net/url" "os" "strconv" "strings" "time" ) type Config struct { ListenAddr string PublicURL *url.URL DatabaseDSN string GiteaURL *url.URL GiteaClientID string GiteaClientSecret string GiteaPackageOwner string GiteaPackageUser string GiteaPackageToken string AllowedGiteaUsers map[string]struct{} CookieSecret []byte DefaultDuration time.Duration DefaultHostLimit int MaxHostLimit int TrustProxyHeaders bool } func LoadConfig() (Config, error) { var cfg Config var err error cfg.ListenAddr = envDefault("TAPM_LISTEN_ADDR", ":8080") cfg.DatabaseDSN = os.Getenv("TAPM_DATABASE_DSN") cfg.GiteaClientID = os.Getenv("TAPM_GITEA_CLIENT_ID") cfg.GiteaClientSecret = os.Getenv("TAPM_GITEA_CLIENT_SECRET") cfg.GiteaPackageOwner = envDefault("TAPM_GITEA_PACKAGE_OWNER", "TAI") cfg.GiteaPackageUser = os.Getenv("TAPM_GITEA_PACKAGE_USERNAME") cfg.GiteaPackageToken = os.Getenv("TAPM_GITEA_PACKAGE_TOKEN") cfg.CookieSecret = []byte(os.Getenv("TAPM_COOKIE_SECRET")) cfg.PublicURL, err = parseAbsoluteURL("TAPM_PUBLIC_URL") if err != nil { return cfg, err } cfg.GiteaURL, err = parseAbsoluteURL("TAPM_GITEA_URL") if err != nil { return cfg, err } cfg.DefaultDuration, err = time.ParseDuration( envDefault("TAPM_DEFAULT_DURATION", "3h"), ) if err != nil || cfg.DefaultDuration <= 0 { return cfg, fmt.Errorf("TAPM_DEFAULT_DURATION must be positive") } cfg.DefaultHostLimit, err = envInt("TAPM_DEFAULT_HOST_LIMIT", 3) if err != nil { return cfg, err } cfg.MaxHostLimit, err = envInt("TAPM_MAX_HOST_LIMIT", 25) if err != nil { return cfg, err } if cfg.DefaultHostLimit < 1 || cfg.MaxHostLimit < cfg.DefaultHostLimit { return cfg, fmt.Errorf("invalid default or maximum host limit") } cfg.TrustProxyHeaders, err = strconv.ParseBool( envDefault("TAPM_TRUST_PROXY_HEADERS", "false"), ) if err != nil { return cfg, fmt.Errorf("TAPM_TRUST_PROXY_HEADERS: %w", err) } cfg.AllowedGiteaUsers = make(map[string]struct{}) for _, user := range strings.Split( envDefault("TAPM_ALLOWED_GITEA_USERS", "taiadmin"), ",", ) { user = strings.ToLower(strings.TrimSpace(user)) if user != "" { cfg.AllowedGiteaUsers[user] = struct{}{} } } required := map[string]string{ "TAPM_DATABASE_DSN": cfg.DatabaseDSN, "TAPM_GITEA_CLIENT_ID": cfg.GiteaClientID, "TAPM_GITEA_CLIENT_SECRET": cfg.GiteaClientSecret, "TAPM_GITEA_PACKAGE_USERNAME": cfg.GiteaPackageUser, "TAPM_GITEA_PACKAGE_TOKEN": cfg.GiteaPackageToken, } for name, value := range required { if strings.TrimSpace(value) == "" { return cfg, fmt.Errorf("%s is required", name) } } if len(cfg.CookieSecret) < 32 { return cfg, fmt.Errorf("TAPM_COOKIE_SECRET must contain at least 32 bytes") } if len(cfg.AllowedGiteaUsers) == 0 { return cfg, fmt.Errorf("at least one Gitea user must be allowed") } return cfg, nil } func parseAbsoluteURL(name string) (*url.URL, error) { raw := os.Getenv(name) parsed, err := url.Parse(raw) if err != nil || parsed.Scheme != "https" || parsed.Host == "" { return nil, fmt.Errorf("%s must be an absolute HTTPS URL", name) } return parsed, nil } func envDefault(name, fallback string) string { if value := os.Getenv(name); value != "" { return value } return fallback } func envInt(name string, fallback int) (int, error) { raw := envDefault(name, strconv.Itoa(fallback)) value, err := strconv.Atoi(raw) if err != nil { return 0, fmt.Errorf("%s must be an integer", name) } return value, nil }