diff --git a/back-end/.gitignore b/back-end/.gitignore index 1d102903..808e9f55 100644 --- a/back-end/.gitignore +++ b/back-end/.gitignore @@ -4,11 +4,11 @@ ! .env.example # 🏗️ Build Binaries (Go) -api -api_server -verify -verify_transfer -worker +/api +/api_server +/verify +/verify_transfer +/worker /bin/ *.exe *.exe~ diff --git a/back-end/cmd/api/middleware/cors.go b/back-end/cmd/api/middleware/cors.go new file mode 100644 index 00000000..9b1551fa --- /dev/null +++ b/back-end/cmd/api/middleware/cors.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "os" + + "github.com/gin-gonic/gin" +) + +// CORSMiddleware sets CORS headers. Allowed origin is read from CORS_ORIGIN env var, +// defaults to the production domain. +func CORSMiddleware() gin.HandlerFunc { + allowedOrigin := os.Getenv("CORS_ORIGIN") + if allowedOrigin == "" { + allowedOrigin = "https://paycif.com" + } + + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + if origin == allowedOrigin || allowedOrigin == "*" { + c.Header("Access-Control-Allow-Origin", origin) + } + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Device-Id, X-Device-Signature, X-Request-ID") + c.Header("Access-Control-Max-Age", "86400") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} diff --git a/back-end/cmd/api/middleware/recovery.go b/back-end/cmd/api/middleware/recovery.go new file mode 100644 index 00000000..b3fc05c5 --- /dev/null +++ b/back-end/cmd/api/middleware/recovery.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "log/slog" + "net/http" + + "github.com/gin-gonic/gin" +) + +// Recovery returns a middleware that recovers from panics and returns 500. +func Recovery() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + slog.Error("Panic recovered", "error", err, "path", c.Request.URL.Path) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + } + }() + c.Next() + } +} diff --git a/back-end/cmd/api/middleware/security_headers.go b/back-end/cmd/api/middleware/security_headers.go new file mode 100644 index 00000000..538e2ca9 --- /dev/null +++ b/back-end/cmd/api/middleware/security_headers.go @@ -0,0 +1,15 @@ +package middleware + +import "github.com/gin-gonic/gin" + +// SecurityHeadersMiddleware adds standard security headers to every response. +func SecurityHeadersMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("X-Content-Type-Options", "nosniff") + c.Header("X-Frame-Options", "DENY") + c.Header("X-XSS-Protection", "1; mode=block") + c.Header("Referrer-Policy", "strict-origin-when-cross-origin") + c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()") + c.Next() + } +} diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..1d75d504 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,3 @@ +BACKEND_URL=https://api.paycif.com +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your_anon_key_here