From 3eb04b1a2bdf9e53231fe862cfd76327371a9741 Mon Sep 17 00:00:00 2001 From: gramanas Date: Sat, 12 Sep 2026 11:24:17 +0300 Subject: Initial multi-tenant blog host Go + Postgres application serving a management dashboard on the base domain and one public blog per subdomain. Markdown posts organised in pages, form-based theme customisation, image uploads stored in Postgres, JWT cookie sessions with CSRF, superadmin user management, RSS feeds. Docker/compose deployment and a Makefile-driven dev environment with seed data. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/config/config.go | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 internal/config/config.go (limited to 'internal/config') diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4972178 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,90 @@ +// Package config reads application settings from the environment. +package config + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +type Config struct { + Addr string // listen address, e.g. ":8080" + BaseDomain string // e.g. "example.com" (no port) + PublicPort string // port to append to generated blog URLs ("" in prod behind a proxy) + DatabaseURL string + JWTSecret []byte + // Bootstrap superadmin created on first start if no superadmin exists. + SuperadminUsername string + SuperadminPassword string + MaxUploadBytes int64 + Dev bool // reload templates/static from disk +} + +func Load() (*Config, error) { + c := &Config{ + Addr: env("ADDR", ":8080"), + BaseDomain: strings.ToLower(strings.TrimSpace(env("BASE_DOMAIN", "blogspace.localhost"))), + PublicPort: env("PUBLIC_PORT", ""), + DatabaseURL: env("DATABASE_URL", "postgres://blogspace:blogspace@localhost:5432/blogspace?sslmode=disable"), + JWTSecret: []byte(env("JWT_SECRET", "")), + SuperadminUsername: env("SUPERADMIN_USERNAME", "admin"), + SuperadminPassword: env("SUPERADMIN_PASSWORD", ""), + Dev: envBool("DEV", false), + } + mb, err := strconv.Atoi(env("MAX_UPLOAD_MB", "5")) + if err != nil || mb <= 0 { + return nil, fmt.Errorf("MAX_UPLOAD_MB must be a positive integer") + } + c.MaxUploadBytes = int64(mb) << 20 + + if len(c.JWTSecret) == 0 { + if !c.Dev { + return nil, fmt.Errorf("JWT_SECRET must be set") + } + c.JWTSecret = []byte("dev-insecure-secret-change-me") + } + if len(c.JWTSecret) < 16 && !c.Dev { + return nil, fmt.Errorf("JWT_SECRET must be at least 16 bytes") + } + if c.BaseDomain == "" { + return nil, fmt.Errorf("BASE_DOMAIN must be set") + } + return c, nil +} + +// BlogURL returns the public URL for a blog subdomain. +func (c *Config) BlogURL(sub string) string { + return "http://" + sub + "." + c.HostWithPort() +} + +// RootURL returns the public URL of the management site. +func (c *Config) RootURL() string { + return "http://" + c.HostWithPort() +} + +func (c *Config) HostWithPort() string { + if c.PublicPort != "" { + return c.BaseDomain + ":" + c.PublicPort + } + return c.BaseDomain +} + +func env(key, def string) string { + if v, ok := os.LookupEnv(key); ok { + return v + } + return def +} + +func envBool(key string, def bool) bool { + v, ok := os.LookupEnv(key) + if !ok { + return def + } + switch strings.ToLower(v) { + case "1", "true", "yes", "on": + return true + } + return false +} -- cgit v1.2.3