237 lines
7.2 KiB
Go
237 lines
7.2 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
// Config holds all configuration for the application.
|
|
type Config struct {
|
|
// Server
|
|
Env string // "development" | "production"
|
|
Port int
|
|
|
|
// MongoDB (content database)
|
|
MongoURI string
|
|
MongoDB string
|
|
|
|
// PostgreSQL (users, relations, URL shortener)
|
|
PostgresURI string
|
|
|
|
// Typesense
|
|
TypesenseURL string
|
|
TypesenseAPIKey string
|
|
|
|
// DragonflyDB / Redis
|
|
RedisURI string
|
|
|
|
// Cloudflare R2 (S3-compatible) — for avatar uploads via presigned URLs.
|
|
R2AccountID string
|
|
R2AccessKeyID string
|
|
R2SecretAccessKey string
|
|
R2Bucket string
|
|
R2PublicURL string // Public base URL for objects, e.g. https://cdn.example.com or the R2 public bucket URL
|
|
|
|
// SMTP — used for password-reset, email-verification, and
|
|
// /feedback admin-notification mails.
|
|
SMTPHost string
|
|
SMTPPort int
|
|
SMTPUsername string
|
|
SMTPPassword string
|
|
SMTPFrom string // "Prabhupāda Says <noreply@prabhupadasays.org>"
|
|
|
|
// Comma-separated FEEDBACK_ADMIN_EMAILS — recipients for the
|
|
// /feedback notification fan-out. Empty disables the email send
|
|
// (the DB row is still written and visible via /stats).
|
|
FeedbackAdminEmails []string
|
|
|
|
// Push Notifications (existing — unchanged).
|
|
FCMCredentialsFile string
|
|
APNsKeyFile string
|
|
APNsKeyID string
|
|
APNsTeamID string
|
|
APNsBundleID string
|
|
|
|
// JWT Auth
|
|
JWTSecret string
|
|
JWTExpiration time.Duration // parsed at load time
|
|
CookieDomain string // empty in dev; ".prabhupadasays.org" in prod
|
|
CookieSecure bool // true in production
|
|
|
|
// Frontend
|
|
FrontendURL string // for CORS + reset/verify links in emails
|
|
|
|
// AdminURL is the admin panel's origin. When set it's added to the CORS
|
|
// allow-list (the admin app is a separate origin from FrontendURL). Empty
|
|
// disables admin-origin CORS (only the main frontend is allowed).
|
|
AdminURL string
|
|
|
|
// RevalidateSecret authenticates BFF→frontend on-demand cache
|
|
// revalidation pings (POST /api/revalidate). Empty disables the pings.
|
|
RevalidateSecret string
|
|
|
|
// BFF public URL — the externally-reachable origin of this server. Used
|
|
// to build the OAuth redirect_uri the provider sees (must match the
|
|
// allow-list configured in the provider's console). In dev this is
|
|
// usually http://localhost:8080.
|
|
BFFPublicURL string
|
|
|
|
// OAuth providers — empty creds disable the corresponding provider.
|
|
GoogleOAuthClientID string
|
|
GoogleOAuthClientSecret string
|
|
FacebookOAuthClientID string
|
|
FacebookOAuthClientSecret string
|
|
|
|
// Soketi (Pusher-compatible WebSocket server) — used to push realtime
|
|
// notifications. Empty App credentials disable realtime delivery (the
|
|
// DB row + email still happen). SoketiHost is the HTTP-API origin the
|
|
// BFF triggers events against (e.g. "ws.prabhupadasays.org"); the
|
|
// frontend connects to the same host over WSS.
|
|
SoketiAppID string
|
|
SoketiAppKey string
|
|
SoketiAppSecret string
|
|
SoketiHost string
|
|
SoketiPort int
|
|
SoketiUseTLS bool
|
|
|
|
// Web Push (VAPID). Empty keys disable push delivery (other channels
|
|
// still run). VAPIDSubject is a mailto: or https: contact URL required
|
|
// by the spec. The public key is also served to the browser to
|
|
// subscribe.
|
|
VAPIDPublicKey string
|
|
VAPIDPrivateKey string
|
|
VAPIDSubject string
|
|
}
|
|
|
|
// Load reads configuration from environment variables (with .env fallback).
|
|
func Load() (*Config, error) {
|
|
_ = godotenv.Load()
|
|
|
|
jwtExp, err := time.ParseDuration(getEnv("JWT_EXPIRATION", "168h"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid JWT_EXPIRATION: %w", err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
Env: getEnv("APP_ENV", "development"),
|
|
Port: getEnvInt("PORT", 8080),
|
|
|
|
MongoURI: getEnv("MONGO_URI", ""),
|
|
MongoDB: getEnv("MONGO_DB", "spsays"),
|
|
|
|
PostgresURI: getEnv("POSTGRES_URI", ""),
|
|
|
|
TypesenseURL: getEnv("TYPESENSE_URL", "https://search.prabhupadasays.org"),
|
|
TypesenseAPIKey: getEnv("TYPESENSE_API_KEY", ""),
|
|
|
|
RedisURI: getEnv("REDIS_URI", "redis://localhost:6379"),
|
|
|
|
R2AccountID: getEnv("R2_ACCOUNT_ID", ""),
|
|
R2AccessKeyID: getEnv("R2_ACCESS_KEY_ID", ""),
|
|
R2SecretAccessKey: getEnv("R2_SECRET_ACCESS_KEY", ""),
|
|
R2Bucket: getEnv("R2_BUCKET", ""),
|
|
R2PublicURL: getEnv("R2_PUBLIC_URL", ""),
|
|
|
|
SMTPHost: getEnv("SMTP_HOST", ""),
|
|
SMTPPort: getEnvInt("SMTP_PORT", 587),
|
|
SMTPUsername: getEnv("SMTP_USERNAME", ""),
|
|
SMTPPassword: getEnv("SMTP_PASSWORD", ""),
|
|
SMTPFrom: getEnv("SMTP_FROM", "noreply@localhost"),
|
|
|
|
FeedbackAdminEmails: parseList(getEnv("FEEDBACK_ADMIN_EMAILS", "")),
|
|
|
|
FCMCredentialsFile: getEnv("FCM_CREDENTIALS_FILE", ""),
|
|
APNsKeyFile: getEnv("APNS_KEY_FILE", ""),
|
|
APNsKeyID: getEnv("APNS_KEY_ID", ""),
|
|
APNsTeamID: getEnv("APNS_TEAM_ID", ""),
|
|
APNsBundleID: getEnv("APNS_BUNDLE_ID", ""),
|
|
|
|
JWTSecret: getEnv("JWT_SECRET", ""),
|
|
JWTExpiration: jwtExp,
|
|
CookieDomain: getEnv("COOKIE_DOMAIN", ""),
|
|
CookieSecure: getEnvBool("COOKIE_SECURE", false),
|
|
|
|
FrontendURL: getEnv("FRONTEND_URL", "http://localhost:3000"),
|
|
AdminURL: getEnv("ADMIN_URL", ""),
|
|
RevalidateSecret: getEnv("REVALIDATE_SECRET", ""),
|
|
|
|
BFFPublicURL: getEnv("BFF_PUBLIC_URL", "http://localhost:8080"),
|
|
|
|
GoogleOAuthClientID: getEnv("GOOGLE_OAUTH_CLIENT_ID", ""),
|
|
GoogleOAuthClientSecret: getEnv("GOOGLE_OAUTH_CLIENT_SECRET", ""),
|
|
FacebookOAuthClientID: getEnv("FACEBOOK_OAUTH_CLIENT_ID", ""),
|
|
FacebookOAuthClientSecret: getEnv("FACEBOOK_OAUTH_CLIENT_SECRET", ""),
|
|
|
|
SoketiAppID: getEnv("SOKETI_APP_ID", ""),
|
|
SoketiAppKey: getEnv("SOKETI_APP_KEY", ""),
|
|
SoketiAppSecret: getEnv("SOKETI_APP_SECRET", ""),
|
|
SoketiHost: getEnv("SOKETI_HOST", "ws.prabhupadasays.org"),
|
|
SoketiPort: getEnvInt("SOKETI_PORT", 443),
|
|
SoketiUseTLS: getEnvBool("SOKETI_USE_TLS", true),
|
|
|
|
VAPIDPublicKey: getEnv("VAPID_PUBLIC_KEY", ""),
|
|
VAPIDPrivateKey: getEnv("VAPID_PRIVATE_KEY", ""),
|
|
VAPIDSubject: getEnv("VAPID_SUBJECT", "mailto:admin@prabhupadasays.org"),
|
|
}
|
|
|
|
if cfg.MongoURI == "" {
|
|
return nil, fmt.Errorf("MONGO_URI is required")
|
|
}
|
|
if cfg.Env == "production" && cfg.JWTSecret == "" {
|
|
return nil, fmt.Errorf("JWT_SECRET is required in production")
|
|
}
|
|
if cfg.JWTSecret == "" {
|
|
// Dev fallback so the server can boot without secrets configured.
|
|
cfg.JWTSecret = "dev-only-jwt-secret-do-not-use-in-production"
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func getEnvInt(key string, fallback int) int {
|
|
if v := os.Getenv(key); v != "" {
|
|
if i, err := strconv.Atoi(v); err == nil {
|
|
return i
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func getEnvBool(key string, fallback bool) bool {
|
|
if v := os.Getenv(key); v != "" {
|
|
if b, err := strconv.ParseBool(v); err == nil {
|
|
return b
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// parseList splits a comma-separated env value, trimming whitespace
|
|
// and discarding empty entries. Returns nil for empty input so
|
|
// callers can range over it safely without a length check.
|
|
func parseList(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(s, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|