diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index a9394b3..96cdda5 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -8,8 +8,11 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" "github.com/go-chi/httprate" + "github.com/unrolled/secure" + "novamd/internal/api" "novamd/internal/auth" "novamd/internal/config" @@ -68,6 +71,25 @@ func main() { r.Use(middleware.Recoverer) r.Use(middleware.RequestID) r.Use(middleware.RealIP) + + // Security headers + r.Use(secure.New(secure.Options{ + SSLRedirect: false, // Let proxy handle HTTPS + SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, + IsDevelopment: cfg.IsDevelopment, + }).Handler) + + // CORS if origins are configured + if len(cfg.CORSOrigins) > 0 { + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: cfg.CORSOrigins, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"}, + AllowCredentials: true, + MaxAge: 300, // Maximum value not ignored by any major browser + })) + } + r.Use(middleware.Timeout(30 * time.Second)) // Set up routes diff --git a/backend/go.mod b/backend/go.mod index 7b034ed..0af2cf4 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,12 +4,14 @@ go 1.23.1 require ( github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.14.1 github.com/go-git/go-git/v5 v5.12.0 github.com/go-playground/validator/v10 v10.22.1 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/mattn/go-sqlite3 v1.14.23 + github.com/unrolled/secure v1.17.0 golang.org/x/crypto v0.21.0 ) diff --git a/backend/go.sum b/backend/go.sum index fa718fd..426c423 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -30,6 +30,8 @@ github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs= github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -91,6 +93,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= +github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 3b9c862..834d2ca 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "time" "novamd/internal/crypto" @@ -15,12 +16,15 @@ type Config struct { WorkDir string StaticPath string Port string + AppURL string + CORSOrigins []string AdminEmail string AdminPassword string EncryptionKey string JWTSigningKey string RateLimitRequests int RateLimitWindow time.Duration + IsDevelopment bool } func DefaultConfig() *Config { @@ -31,6 +35,7 @@ func DefaultConfig() *Config { Port: "8080", RateLimitRequests: int(10), RateLimitWindow: time.Minute, + IsDevelopment: false, } } @@ -51,6 +56,10 @@ func (c *Config) Validate() error { func Load() (*Config, error) { config := DefaultConfig() + if env := os.Getenv("NOVAMD_ENV"); env != "" { + config.IsDevelopment = env == "development" + } + if dbPath := os.Getenv("NOVAMD_DB_PATH"); dbPath != "" { config.DBPath = dbPath } @@ -73,6 +82,14 @@ func Load() (*Config, error) { config.Port = port } + if appURL := os.Getenv("NOVAMD_APP_URL"); appURL != "" { + config.AppURL = appURL + } + + if corsOrigins := os.Getenv("NOVAMD_CORS_ORIGINS"); corsOrigins != "" { + config.CORSOrigins = strings.Split(corsOrigins, ",") + } + config.AdminEmail = os.Getenv("NOVAMD_ADMIN_EMAIL") config.AdminPassword = os.Getenv("NOVAMD_ADMIN_PASSWORD") config.EncryptionKey = os.Getenv("NOVAMD_ENCRYPTION_KEY")