initiall commit

This commit is contained in:
Tomas Dvorak
2026-04-10 12:03:31 +02:00
commit 7ddfb1f52b
276 changed files with 37629 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
package config
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
)
type Config struct {
Env string
ServerPort string
DatabaseURL string
DragonflyURL string
StorageRoot string
AuthInternalBaseURL string
PublicURL string
UserRateLimitPerMin int
APIKeyRateLimitPerMin int
JWTIssuer string
JWTAudience string
JWTSecret string
JWTTTLSeconds int
MailFrom string
ResendAPIKey string
SMTPHost string
SMTPPort int
SMTPUser string
SMTPPassword string
SMTPSecure bool
}
func Load() (Config, error) {
cfg := Config{
Env: getenv("NODE_ENV", "development"),
ServerPort: getenv("BACKEND_PORT", "8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
DragonflyURL: getenv("DRAGONFLY_URL", "redis://localhost:6379/0"),
StorageRoot: getenv("BACKEND_STORAGE_ROOT", "./tmp/storage"),
AuthInternalBaseURL: getenv("AUTH_INTERNAL_BASE_URL", "http://auth:3001"),
PublicURL: getenv("VITE_APP_URL", "http://localhost"),
UserRateLimitPerMin: 240,
APIKeyRateLimitPerMin: 600,
JWTIssuer: getenv("JWT_ISSUER", "primora-auth"),
JWTAudience: getenv("JWT_AUDIENCE", "primora-api"),
JWTSecret: os.Getenv("JWT_SECRET"),
MailFrom: getenv("MAIL_FROM", "Primora <no-reply@primora.local>"),
ResendAPIKey: os.Getenv("RESEND_API_KEY"),
SMTPHost: getenv("SMTP_HOST", "localhost"),
SMTPUser: os.Getenv("SMTP_USER"),
SMTPPassword: os.Getenv("SMTP_PASSWORD"),
}
smtpPort, err := strconv.Atoi(getenv("SMTP_PORT", "1025"))
if err != nil {
return Config{}, fmt.Errorf("parse SMTP_PORT: %w", err)
}
cfg.SMTPPort = smtpPort
jwtTTLSeconds, err := strconv.Atoi(getenv("JWT_TTL_SECONDS", "900"))
if err != nil {
return Config{}, fmt.Errorf("parse JWT_TTL_SECONDS: %w", err)
}
cfg.JWTTTLSeconds = jwtTTLSeconds
userRateLimitPerMin, err := parseNonNegativeIntEnv("USER_RATE_LIMIT_PER_MINUTE", cfg.UserRateLimitPerMin)
if err != nil {
return Config{}, err
}
cfg.UserRateLimitPerMin = userRateLimitPerMin
apiKeyRateLimitPerMin, err := parseNonNegativeIntEnv("API_KEY_RATE_LIMIT_PER_MINUTE", cfg.APIKeyRateLimitPerMin)
if err != nil {
return Config{}, err
}
cfg.APIKeyRateLimitPerMin = apiKeyRateLimitPerMin
smtpSecure, err := strconv.ParseBool(getenv("SMTP_SECURE", "false"))
if err != nil {
return Config{}, fmt.Errorf("parse SMTP_SECURE: %w", err)
}
cfg.SMTPSecure = smtpSecure
var missing []string
if cfg.DatabaseURL == "" {
missing = append(missing, "DATABASE_URL")
}
if cfg.JWTSecret == "" {
missing = append(missing, "JWT_SECRET")
}
if cfg.StorageRoot == "" {
missing = append(missing, "BACKEND_STORAGE_ROOT")
}
if cfg.AuthInternalBaseURL == "" {
missing = append(missing, "AUTH_INTERNAL_BASE_URL")
}
if cfg.ResendAPIKey == "" && cfg.SMTPHost == "" {
missing = append(missing, "RESEND_API_KEY or SMTP_HOST")
}
if len(missing) > 0 {
return Config{}, errors.New("missing required environment values: " + strings.Join(missing, ", "))
}
return cfg, nil
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func parseNonNegativeIntEnv(key string, fallback int) (int, error) {
raw := os.Getenv(key)
if raw == "" {
return fallback, nil
}
value, err := strconv.Atoi(raw)
if err != nil {
return 0, fmt.Errorf("parse %s: %w", key, err)
}
if value < 0 {
return 0, fmt.Errorf("%s must be >= 0", key)
}
return value, nil
}
@@ -0,0 +1,58 @@
package config
import (
"strings"
"testing"
)
func setRequiredEnv(t *testing.T) {
t.Helper()
t.Setenv("DATABASE_URL", "postgres://primora:primora@localhost:5432/primora?sslmode=disable")
t.Setenv("JWT_SECRET", "test-secret")
t.Setenv("BACKEND_STORAGE_ROOT", "./tmp/storage")
t.Setenv("AUTH_INTERNAL_BASE_URL", "http://auth:3001")
t.Setenv("SMTP_HOST", "mailpit")
}
func TestLoadRateLimitDefaults(t *testing.T) {
setRequiredEnv(t)
t.Setenv("USER_RATE_LIMIT_PER_MINUTE", "")
t.Setenv("API_KEY_RATE_LIMIT_PER_MINUTE", "")
cfg, err := Load()
if err != nil {
t.Fatalf("load config: %v", err)
}
if cfg.UserRateLimitPerMin != 240 {
t.Fatalf("unexpected USER_RATE_LIMIT_PER_MINUTE default: %d", cfg.UserRateLimitPerMin)
}
if cfg.APIKeyRateLimitPerMin != 600 {
t.Fatalf("unexpected API_KEY_RATE_LIMIT_PER_MINUTE default: %d", cfg.APIKeyRateLimitPerMin)
}
}
func TestLoadRateLimitRejectsInvalidNumber(t *testing.T) {
setRequiredEnv(t)
t.Setenv("USER_RATE_LIMIT_PER_MINUTE", "not-a-number")
_, err := Load()
if err == nil {
t.Fatalf("expected parse error")
}
if !strings.Contains(err.Error(), "parse USER_RATE_LIMIT_PER_MINUTE") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadRateLimitRejectsNegative(t *testing.T) {
setRequiredEnv(t)
t.Setenv("API_KEY_RATE_LIMIT_PER_MINUTE", "-1")
_, err := Load()
if err == nil {
t.Fatalf("expected validation error")
}
if !strings.Contains(err.Error(), "API_KEY_RATE_LIMIT_PER_MINUTE must be >= 0") {
t.Fatalf("unexpected error: %v", err)
}
}