mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
- core.integrations (migration 00005) with AES-256-GCM-encrypted credentials (PRIMORA_ENCRYPTION_KEY, hex/base64, dev fallback); has_credentials serialization only — secrets never leave the server - /projects/:id/integrations list/create/delete/test; health check hits the stored base URL with a 5s timeout and records status - Rybbit connector: GET …/integrations/:id/analytics proxies /api/organizations + /api/sites/* server-side into a normalized shape - core.webhooks + core.webhook_deliveries; CRUD, deliveries, test, and POST …/deploy-markers. Events: issue.created (new fingerprint on ingest), deploy.marker, webhook.test. X-Primora-Signature HMAC-SHA256 over the raw body; in-process dispatcher, 3 attempts, quadratic backoff, 10s timeout, pending-scan on boot - HTTPS enforced for public targets; HTTP allowed for loopback/private/ self-hosted sinks - Integrations page (connectors + webhooks tabs, inline analytics, delivery log, secret-shown-once), demo-mode stubs - 12 new primora_* MCP tools - workspace 0.4.0 Authored By: TDvorak <[email protected]>
154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Config struct {
|
|
Env string
|
|
ServerPort string
|
|
DatabaseURL string
|
|
DragonflyURL string
|
|
StorageRoot string
|
|
DBXDataDir 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
|
|
|
|
EncryptionKey string
|
|
}
|
|
|
|
// devEncryptionKey is the built-in credential key for local development only.
|
|
// Set PRIMORA_ENCRYPTION_KEY in any real deployment — credentials written with
|
|
// this key are readable by anyone with database access.
|
|
const devEncryptionKey = "3031323334353637383961626364656630313233343536373839616263646566"
|
|
|
|
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"),
|
|
DBXDataDir: os.Getenv("DBX_DATA_DIR"),
|
|
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 <[email protected]>"),
|
|
ResendAPIKey: os.Getenv("RESEND_API_KEY"),
|
|
SMTPHost: getenv("SMTP_HOST", "localhost"),
|
|
SMTPUser: os.Getenv("SMTP_USER"),
|
|
SMTPPassword: os.Getenv("SMTP_PASSWORD"),
|
|
EncryptionKey: os.Getenv("PRIMORA_ENCRYPTION_KEY"),
|
|
}
|
|
|
|
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 cfg.EncryptionKey == "" {
|
|
if cfg.Env == "production" {
|
|
missing = append(missing, "PRIMORA_ENCRYPTION_KEY")
|
|
} else {
|
|
cfg.EncryptionKey = devEncryptionKey
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
return Config{}, errors.New("missing required environment values: " + strings.Join(missing, ", "))
|
|
}
|
|
|
|
if cfg.DBXDataDir == "" {
|
|
cfg.DBXDataDir = filepath.Join(cfg.StorageRoot, "dbx")
|
|
}
|
|
|
|
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
|
|
}
|