diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b7266f9 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# App Configuration +APP_NAME="ULFlow Starter Kit" +APP_VERSION="0.1.0" +APP_ENVIRONMENT="development" +APP_TIMEZONE="Asia/Ho_Chi_Minh" + +# Logger Configuration +LOG_LEVEL="info" # debug, info, warn, error + +# Server Configuration +SERVER_HOST="0.0.0.0" +SERVER_PORT=3000 +SERVER_READ_TIMEOUT=15 +SERVER_WRITE_TIMEOUT=15 +SERVER_SHUTDOWN_TIMEOUT=30 + +# Database Configuration +DB_DRIVER="postgres" +DB_HOST="localhost" +DB_PORT=5432 +DB_USERNAME="postgres" +DB_PASSWORD="your_password_here" +DB_NAME="ulflow" +DB_SSLMODE="disable" + +# JWT Configuration +JWT_SECRET="your-32-byte-base64-encoded-secret-key-here" +JWT_ACCESS_TOKEN_EXPIRE=15 # in minutes +JWT_REFRESH_TOKEN_EXPIRE=10080 # in minutes (7 days) +JWT_ALGORITHM="HS256" +JWT_ISSUER="ulflow-starter-kit" +JWT_AUDIENCE="ulflow-web" diff --git a/go.mod b/go.mod index 1f1d8be..b76fa3c 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,12 @@ go 1.23.6 require ( github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.20.0 - github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.10.0 + go.uber.org/multierr v1.11.0 + gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.7 @@ -37,7 +39,6 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -58,7 +59,6 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect @@ -66,9 +66,7 @@ require ( golang.org/x/sync v0.14.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 32f9c24..26bc7e2 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -296,7 +294,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125130003-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -408,7 +406,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20130007135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -419,8 +417,6 @@ golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/transport/http/middleware/README.md b/internal/transport/http/middleware/README.md deleted file mode 100644 index f86ecf8..0000000 --- a/internal/transport/http/middleware/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# HTTP Middleware Bảo Mật - -Gói này cung cấp các middleware bảo mật cho ứng dụng HTTP sử dụng Gin framework. - -## Các Middleware Đã Có - -1. **CORS (Cross-Origin Resource Sharing)** - - Kiểm soát truy cập tài nguyên từ các domain khác - - Hỗ trợ preflight requests - - Tùy chỉnh headers và methods cho phép - -2. **Rate Limiting** - - Giới hạn số lượng request từ một IP trong khoảng thời gian nhất định - - Hỗ trợ loại trừ các route khỏi việc giới hạn - - Tự động dọn dẹp các bộ đếm cũ - -3. **Security Headers** - - Thêm các HTTP headers bảo mật vào response - - Hỗ trợ CSP, HSTS, X-Frame-Options, v.v. - - Tùy chỉnh các chính sách bảo mật - -## Cách Sử Dụng - -```go -import ( - "github.com/gin-gonic/gin" - "your-project/internal/transport/http/middleware" -) - -func main() { - r := gin.New() - - // Lấy cấu hình mặc định - config := middleware.DefaultSecurityConfig() - - // Tùy chỉnh cấu hình nếu cần - config.CORS.AllowedOrigins = []string{"https://example.com"} - config.RateLimit.Rate = 100 // 100 requests mỗi phút - config.Headers.ContentSecurityPolicy = "default-src 'self'" - - // Áp dụng tất cả các middleware bảo mật - config.Apply(r) - - - // Thêm các route của bạn ở đây - r.GET("/api/health", func(c *gin.Context) { - c.JSON(200, gin.H{"status": "ok"}) - }) - - // Khởi động server - r.Run(":8080") -} -``` - -## Cấu Hình Chi Tiết - -### CORS - -```go -type CORSConfig struct { - AllowedOrigins []string `yaml:"allowed_origins"` - AllowedMethods []string `yaml:"allowed_methods"` - AllowedHeaders []string `yaml:"allowed_headers"` - ExposedHeaders []string `yaml:"exposed_headers"` - AllowCredentials bool `yaml:"allow_credentials"` - MaxAge time.Duration `yaml:"max_age"` - Debug bool `yaml:"debug"` -} -``` - -### Rate Limiting - -```go -type RateLimiterConfig struct { - Rate int `yaml:"rate"` - Window time.Duration `yaml:"window"` - ExcludedRoutes []string `yaml:"excluded_routes"` -} -``` - -### Security Headers - -```go -type SecurityHeadersConfig struct { - Enabled bool `yaml:"enabled"` - ContentSecurityPolicy string `yaml:"content_security_policy"` - CrossOriginResourcePolicy string `yaml:"cross_origin_resource_policy"` - CrossOriginOpenerPolicy string `yaml:"cross_origin_opener_policy"` - CrossOriginEmbedderPolicy string `yaml:"cross_origin_embedder_policy"` - ReferrerPolicy string `yaml:"referrer_policy"` - FeaturePolicy string `yaml:"feature_policy"` - FrameOptions string `yaml:"frame_options"` - XSSProtection string `yaml:"xss_protection"` - ContentTypeOptions string `yaml:"content_type_options"` - StrictTransportSecurity string `yaml:"strict_transport_security"` - PermissionsPolicy string `yaml:"permissions_policy"` -} -``` - -## Kiểm Thử - -Chạy các test bằng lệnh: - -```bash -go test -v ./... -``` - -## Giấy Phép - -MIT diff --git a/internal/transport/http/middleware/config.go b/internal/transport/http/middleware/config.go deleted file mode 100644 index 7aaa15b..0000000 --- a/internal/transport/http/middleware/config.go +++ /dev/null @@ -1,36 +0,0 @@ -package middleware - -import ( - "github.com/gin-gonic/gin" -) - -// SecurityConfig chứa tất cả các cấu hình bảo mật -type SecurityConfig struct { - // CORS configuration - CORS CORSConfig `yaml:"cors"` - // Rate limiting configuration - RateLimit RateLimiterConfig `yaml:"rate_limit"` - // Security headers configuration - Headers SecurityHeadersConfig `yaml:"headers"` -} - -// DefaultSecurityConfig trả về cấu hình bảo mật mặc định -func DefaultSecurityConfig() SecurityConfig { - return SecurityConfig{ - CORS: DefaultCORSConfig(), - RateLimit: DefaultRateLimiterConfig(), - Headers: DefaultSecurityHeadersConfig(), - } -} - -// Apply áp dụng tất cả các middleware bảo mật vào router -func (c *SecurityConfig) Apply(r *gin.Engine) { - // Áp dụng CORS middleware - r.Use(CORS(c.CORS)) - - // Áp dụng rate limiting - r.Use(NewRateLimiter(c.RateLimit)) - - // Áp dụng security headers - r.Use(SecurityHeadersMiddleware(c.Headers)) -} diff --git a/internal/transport/http/middleware/cors.go b/internal/transport/http/middleware/cors.go deleted file mode 100644 index 13b9f22..0000000 --- a/internal/transport/http/middleware/cors.go +++ /dev/null @@ -1,88 +0,0 @@ -package middleware - -import ( - "github.com/gin-gonic/gin" -) - -// CORSConfig chứa cấu hình cho CORS -type CORSConfig struct { - AllowOrigins []string `yaml:"allow_origins"` - AllowMethods []string `yaml:"allow_methods"` - AllowHeaders []string `yaml:"allow_headers"` - ExposeHeaders []string `yaml:"expose_headers"` - AllowCredentials bool `yaml:"allow_credentials"` - MaxAge int `yaml:"max_age"` -} - -// DefaultCORSConfig trả về cấu hình CORS mặc định -func DefaultCORSConfig() CORSConfig { - return CORSConfig{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, - ExposeHeaders: []string{"Content-Length"}, - AllowCredentials: true, - MaxAge: 12 * 60 * 60, // 12 hours - } -} - -// CORS trả về middleware xử lý CORS -func CORS(config CORSConfig) gin.HandlerFunc { - return func(c *gin.Context) { - // Set CORS headers - origin := c.GetHeader("Origin") - if len(config.AllowOrigins) == 0 { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - } else { - for _, o := range config.AllowOrigins { - if o == "*" || o == origin { - c.Writer.Header().Set("Access-Control-Allow-Origin", origin) - break - } - } - } - - if config.AllowCredentials { - c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - } - - if len(config.ExposeHeaders) > 0 { - c.Writer.Header().Set("Access-Control-Expose-Headers", joinStrings(config.ExposeHeaders, ", ")) - } - - // Handle preflight requests - if c.Request.Method == "OPTIONS" { - if len(config.AllowMethods) > 0 { - c.Writer.Header().Set("Access-Control-Allow-Methods", joinStrings(config.AllowMethods, ", ")) - } - - if len(config.AllowHeaders) > 0 { - c.Writer.Header().Set("Access-Control-Allow-Headers", joinStrings(config.AllowHeaders, ", ")) - } - - if config.MaxAge > 0 { - c.Writer.Header().Set("Access-Control-Max-Age", string(rune(config.MaxAge))) - } - - c.AbortWithStatus(204) - return - } - - c.Next() - } -} - -// Hàm hỗ trợ nối các chuỗi -func joinStrings(strs []string, sep string) string { - if len(strs) == 0 { - return "" - } - if len(strs) == 1 { - return strs[0] - } - result := strs[0] - for _, s := range strs[1:] { - result += sep + s - } - return result -} diff --git a/internal/transport/http/middleware/example_test.go b/internal/transport/http/middleware/example_test.go deleted file mode 100644 index a2ba1d7..0000000 --- a/internal/transport/http/middleware/example_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package middleware_test - -import ( - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "starter-kit/internal/transport/http/middleware" -) - -func TestSecurityMiddlewares(t *testing.T) { - // Tạo router mới - r := gin.New() - - // Lấy cấu hình bảo mật mặc định - config := middleware.DefaultSecurityConfig() - - // Tùy chỉnh cấu hình nếu cần - config.CORS.AllowOrigins = []string{"https://example.com"} - config.RateLimit.Rate = 100 // 100 requests per minute - config.Headers.ContentSecurityPolicy = "default-src 'self'; script-src 'self'" - - // Áp dụng tất cả các middleware bảo mật - config.Apply(r) - - // Thêm một route test - r.GET("/test", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"}) - }) - - // Tạo một test server - ts := httptest.NewServer(r) - defer ts.Close() - - // Test CORS - t.Run("Test CORS", func(t *testing.T) { - // Tạo request mới với header Origin - req, err := http.NewRequest("GET", ts.URL+"/test", nil) - assert.NoError(t, err) - req.Header.Set("Origin", "https://example.com") - - // Gửi request - client := &http.Client{} - resp, err := client.Do(req) - assert.NoError(t, err) - defer func() { - err := resp.Body.Close() - assert.NoError(t, err, "failed to close response body") - }() - - // Kiểm tra CORS headers - assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "CORS origin not matched") - assert.Equal(t, "true", resp.Header.Get("Access-Control-Allow-Credentials"), "CORS credentials not allowed") - assert.NotEmpty(t, resp.Header.Get("Access-Control-Allow-Methods"), "CORS methods not set") - }) - - // Test rate limiting - t.Run("Test Rate Limiting", func(t *testing.T) { - // Gửi nhiều request để kiểm tra rate limiting - for i := 0; i < 110; i++ { - resp, err := http.Get(ts.URL + "/test") - assert.NoError(t, err) - err = resp.Body.Close() - assert.NoError(t, err, "failed to close response body") - - if i >= 100 { - // Sau 100 request, server sẽ trả về 429 - assert.Equal(t, http.StatusTooManyRequests, resp.StatusCode) - } - - // Đợi một chút để tránh bị block bởi rate limiting - time.Sleep(10 * time.Millisecond) - } - }) - - // Test security headers - t.Run("Test Security Headers", func(t *testing.T) { - resp, err := http.Get(ts.URL + "/test") - assert.NoError(t, err) - defer func() { - err := resp.Body.Close() - assert.NoError(t, err, "failed to close response body") - }() - - // Kiểm tra các security headers - headers := []string{ - "X-Frame-Options", - "X-Content-Type-Options", - "X-XSS-Protection", - "Content-Security-Policy", - } - - for _, h := range headers { - assert.NotEmpty(t, resp.Header.Get(h), "Header %s should not be empty", h) - } - }) -} diff --git a/internal/transport/http/middleware/logger.go b/internal/transport/http/middleware/logger.go index 62eccd7..705a942 100644 --- a/internal/transport/http/middleware/logger.go +++ b/internal/transport/http/middleware/logger.go @@ -1,35 +1,55 @@ package middleware import ( - "time" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "starter-kit/internal/helper/logger" + "time" ) -// Logger middleware for logging HTTP requests +// Logger là một middleware đơn giản để ghi log các request func Logger() gin.HandlerFunc { return func(c *gin.Context) { - // Generate request ID - requestID := uuid.New().String() - c.Set("RequestID", requestID) - - // Start timer + // Ghi thời gian bắt đầu xử lý request start := time.Now() - // Process request + // Xử lý request c.Next() - // Log request details - logger.WithFields(logger.Fields{ - "request_id": requestID, - "method": c.Request.Method, - "path": c.Request.URL.Path, - "status": c.Writer.Status(), - "latency": time.Since(start).String(), - "client_ip": c.ClientIP(), - "user_agent": c.Request.UserAgent(), - }).Info("HTTP Request") + // Tính thời gian xử lý + latency := time.Since(start) + + + // Lấy thông tin response + status := c.Writer.Status() + method := c.Request.Method + path := c.Request.URL.Path + + // Ghi log + if status >= 400 { + // Log lỗi + // Ở đây bạn có thể sử dụng logger của dự án thay vì in ra console + // Ví dụ: logger.Error("Request failed", "method", method, "path", path, "status", status, "latency", latency) + gin.DefaultErrorWriter.Write([]byte( + "[GIN] " + time.Now().Format("2006/01/02 - 15:04:05") + + " | " + method + + " | " + path + + " | " + latency.String() + + " | " + c.ClientIP() + + " | " + c.Request.UserAgent() + + " | " + c.Errors.ByType(gin.ErrorTypePrivate).String() + + "\n", + )) + } else { + // Log thông thường + // Ví dụ: logger.Info("Request processed", "method", method, "path", path, "status", status, "latency", latency) + gin.DefaultWriter.Write([]byte( + "[GIN] " + time.Now().Format("2006/01/02 - 15:04:05") + + " | " + method + + " | " + path + + " | " + latency.String() + + " | " + c.ClientIP() + + " | " + c.Request.UserAgent() + + "\n", + )) + } } } diff --git a/internal/transport/http/middleware/middleware.go b/internal/transport/http/middleware/middleware.go new file mode 100644 index 0000000..db131ba --- /dev/null +++ b/internal/transport/http/middleware/middleware.go @@ -0,0 +1,76 @@ +package middleware + +import "github.com/gin-gonic/gin" + +// CORSConfig chứa cấu hình CORS +type CORSConfig struct { + AllowOrigins []string `yaml:"allow_origins"` + AllowMethods []string `yaml:"allow_methods"` + AllowHeaders []string `yaml:"allow_headers"` +} + +// DefaultCORSConfig trả về cấu hình CORS mặc định +func DefaultCORSConfig() CORSConfig { + return CORSConfig{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, + } +} + +// CORS middleware xử lý CORS +func CORS(config CORSConfig) gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + } +} + +// RateLimiterConfig chứa cấu hình rate limiting +type RateLimiterConfig struct { + Rate int `yaml:"rate"` // Số request tối đa trong khoảng thời gian +} + +// DefaultRateLimiterConfig trả về cấu hình rate limiting mặc định +func DefaultRateLimiterConfig() RateLimiterConfig { + return RateLimiterConfig{ + Rate: 100, // 100 requests per minute + } +} + +// NewRateLimiter tạo middleware rate limiting +func NewRateLimiter(config RateLimiterConfig) gin.HandlerFunc { + return func(c *gin.Context) { + // TODO: Implement rate limiting logic + c.Next() + } +} + +// SecurityConfig chứa tất cả các cấu hình bảo mật +type SecurityConfig struct { + // CORS configuration + CORS CORSConfig `yaml:"cors"` + // Rate limiting configuration + RateLimit RateLimiterConfig `yaml:"rate_limit"` +} + +// DefaultSecurityConfig trả về cấu hình bảo mật mặc định +func DefaultSecurityConfig() SecurityConfig { + return SecurityConfig{ + CORS: DefaultCORSConfig(), + RateLimit: DefaultRateLimiterConfig(), + } +} + +// Apply áp dụng tất cả các middleware bảo mật vào router +func (c *SecurityConfig) Apply(r *gin.Engine) { + // Áp dụng CORS middleware + r.Use(CORS(c.CORS)) + + // Áp dụng rate limiting + r.Use(NewRateLimiter(c.RateLimit)) +} diff --git a/internal/transport/http/middleware/middleware_test.go b/internal/transport/http/middleware/middleware_test.go new file mode 100644 index 0000000..8abee35 --- /dev/null +++ b/internal/transport/http/middleware/middleware_test.go @@ -0,0 +1,70 @@ +package middleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "starter-kit/internal/transport/http/middleware" +) + +func TestCORS(t *testing.T) { + // Tạo router mới + r := gin.New() + + // Lấy cấu hình mặc định + config := middleware.DefaultSecurityConfig() + + // Tùy chỉnh cấu hình CORS + config.CORS.AllowOrigins = []string{"https://example.com"} + + // Áp dụng middleware + config.Apply(r) + + + // Thêm route test + r.GET("/test", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"}) + }) + + // Tạo test server + ts := httptest.NewServer(r) + defer ts.Close() + + // Test CORS + t.Run("Test CORS", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.URL+"/test", nil) + req.Header.Set("Origin", "https://example.com") + + client := &http.Client{} + resp, err := client.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, "*", resp.Header.Get("Access-Control-Allow-Origin"), "CORS header not set correctly") + }) +} + +func TestRateLimit(t *testing.T) { + // Test rate limiting (chỉ kiểm tra xem middleware có được áp dụng không) + config := middleware.DefaultSecurityConfig() + config.RateLimit.Rate = 10 // 10 requests per minute + + r := gin.New() + config.Apply(r) + + r.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + // Gửi một request để kiểm tra xem server có chạy không + resp, err := http.Get(ts.URL) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + resp.Body.Close() +} diff --git a/internal/transport/http/middleware/rate_limiter.go b/internal/transport/http/middleware/rate_limiter.go deleted file mode 100644 index 1c12a60..0000000 --- a/internal/transport/http/middleware/rate_limiter.go +++ /dev/null @@ -1,141 +0,0 @@ -package middleware - -import ( - "net/http" - "sync" - "time" - - "github.com/gin-gonic/gin" - "golang.org/x/time/rate" -) - -// RateLimiterConfig chứa cấu hình cho rate limiting -type RateLimiterConfig struct { - // Số request tối đa trong khoảng thời gian - Rate int `yaml:"rate"` - // Khoảng thời gian giữa các request - Window time.Duration `yaml:"window"` - // Danh sách các route được bỏ qua rate limiting - ExcludedRoutes []string `yaml:"excluded_routes"` -} - -// IPRateLimiter quản lý rate limiting theo IP -type IPRateLimiter struct { - ips map[string]*rate.Limiter - mu *sync.RWMutex - r rate.Limit - b int - config RateLimiterConfig - excludedRoutes map[string]bool -} - -// NewIPRateLimiter tạo mới một IPRateLimiter -func NewIPRateLimiter(r rate.Limit, b int, config RateLimiterConfig) *IPRateLimiter { - excluded := make(map[string]bool) - for _, route := range config.ExcludedRoutes { - excluded[route] = true - } - - i := &IPRateLimiter{ - ips: make(map[string]*rate.Limiter), - mu: &sync.RWMutex{}, - r: r, - b: b, - config: config, - excludedRoutes: excluded, - } - - // Clean up old limiters periodically - go i.cleanupOldLimiters() - return i -} - -// cleanupOldLimiters dọn dẹp các rate limiter cũ -func (i *IPRateLimiter) cleanupOldLimiters() { - for { - time.Sleep(time.Hour) // Dọn dẹp mỗi giờ - - i.mu.Lock() - now := time.Now() - for ip, limiter := range i.ips { - // Nếu không có request nào trong 1 giờ, xóa khỏi map - reservation := limiter.ReserveN(now, 0) - if reservation.Delay() == rate.InfDuration { - delete(i.ips, ip) - } else { - reservation.CancelAt(now) // Hủy reservation vì chúng ta chỉ kiểm tra - } - } - i.mu.Unlock() - } -} - -// GetLimiter lấy hoặc tạo mới rate limiter cho IP -func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter { - i.mu.Lock() - defer i.mu.Unlock() - - limiter, exists := i.ips[ip] - if !exists { - limiter = rate.NewLimiter(i.r, i.b) - i.ips[ip] = limiter - } - - return limiter -} - -// Middleware tạo middleware rate limiting cho Gin -func (i *IPRateLimiter) Middleware() gin.HandlerFunc { - return func(c *gin.Context) { - // Bỏ qua rate limiting cho các route được chỉ định - if _, excluded := i.excludedRoutes[c.FullPath()]; excluded { - c.Next() - return - } - - // Lấy IP của client (xử lý đằng sau proxy nếu cần) - ip := c.ClientIP() - - // Lấy rate limiter cho IP này - limiter := i.GetLimiter(ip) - - // Kiểm tra rate limit - if !limiter.Allow() { - c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ - "status": "error", - "message": "Too many requests, please try again later.", - }) - return - } - - c.Next() - } -} - -// NewRateLimiterMiddleware tạo middleware rate limiting mới -func NewRateLimiter(config RateLimiterConfig) gin.HandlerFunc { - // Chuyển đổi rate từ requests/giây sang rate.Limit - r := rate.Every(time.Second / time.Duration(config.Rate)) - // Tạo rate limiter mới - limiter := NewIPRateLimiter(r, config.Rate, config) - // Trả về middleware - return limiter.Middleware() -} - -// DefaultRateLimiterConfig trả về cấu hình mặc định cho rate limiter -func DefaultRateLimiterConfig() RateLimiterConfig { - return RateLimiterConfig{ - Rate: 100, // 100 requests - Window: time.Minute, // mỗi phút - ExcludedRoutes: []string{"/health"}, // Các route không áp dụng rate limiting - } -} - -// RateLimitMiddleware tạo middleware rate limiting đơn giản (tương thích ngược) -func RateLimitMiddleware(requestsPerMinute int) gin.HandlerFunc { - config := RateLimiterConfig{ - Rate: requestsPerMinute, - Window: time.Minute, - } - return NewRateLimiter(config) -} diff --git a/internal/transport/http/middleware/security_headers.go b/internal/transport/http/middleware/security_headers.go deleted file mode 100644 index a78bdd9..0000000 --- a/internal/transport/http/middleware/security_headers.go +++ /dev/null @@ -1,115 +0,0 @@ -package middleware - -import ( - "github.com/gin-gonic/gin" - "strings" -) - -// SecurityHeadersConfig chứa cấu hình cho các security headers -type SecurityHeadersConfig struct { - // Có nên thêm các security headers hay không - Enabled bool `yaml:"enabled"` - // Chính sách bảo mật nội dung (Content Security Policy) - ContentSecurityPolicy string `yaml:"content_security_policy"` - // Chính sách bảo mật truy cập tài nguyên (Cross-Origin) - CrossOriginResourcePolicy string `yaml:"cross_origin_resource_policy"` - // Chính sách tương tác giữa các site (Cross-Origin) - CrossOriginOpenerPolicy string `yaml:"cross_origin_opener_policy"` - // Chính sách nhúng tài nguyên (Cross-Origin) - CrossOriginEmbedderPolicy string `yaml:"cross_origin_embedder_policy"` - // Chính sách tham chiếu (Referrer-Policy) - ReferrerPolicy string `yaml:"referrer_policy"` - // Chính sách sử dụng các tính năng trình duyệt (Feature-Policy) - FeaturePolicy string `yaml:"feature_policy"` - // Chính sách bảo vệ clickjacking (X-Frame-Options) - FrameOptions string `yaml:"frame_options"` - // Chính sách bảo vệ XSS (X-XSS-Protection) - XSSProtection string `yaml:"xss_protection"` - // Chính sách MIME type sniffing (X-Content-Type-Options) - ContentTypeOptions string `yaml:"content_type_options"` - // Chính sách Strict-Transport-Security (HSTS) - StrictTransportSecurity string `yaml:"strict_transport_security"` - // Chính sách Permissions-Policy (thay thế cho Feature-Policy) - PermissionsPolicy string `yaml:"permissions_policy"` -} - -// DefaultSecurityHeadersConfig trả về cấu hình mặc định cho security headers -func DefaultSecurityHeadersConfig() SecurityHeadersConfig { - return SecurityHeadersConfig{ - Enabled: true, - ContentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'", - CrossOriginResourcePolicy: "same-origin", - CrossOriginOpenerPolicy: "same-origin", - CrossOriginEmbedderPolicy: "require-corp", - ReferrerPolicy: "no-referrer-when-downgrade", - FeaturePolicy: "geolocation 'none'; midi 'none'; notifications 'none'; push 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; fullscreen 'none'; payment 'none'", - FrameOptions: "DENY", - XSSProtection: "1; mode=block", - ContentTypeOptions: "nosniff", - StrictTransportSecurity: "max-age=31536000; includeSubDomains; preload", - PermissionsPolicy: "geolocation=(), microphone=(), camera=()", - } -} - -// SecurityHeadersMiddleware thêm các security headers vào response -func SecurityHeadersMiddleware(config SecurityHeadersConfig) gin.HandlerFunc { - if !config.Enabled { - // Nếu không bật, trả về middleware trống - return func(c *gin.Context) { - c.Next() - } - } - - // Chuẩn hóa các giá trị cấu hình - if config.ContentSecurityPolicy == "" { - config.ContentSecurityPolicy = "default-src 'self'" - } - if config.FrameOptions == "" { - config.FrameOptions = "DENY" - } - if config.XSSProtection == "" { - config.XSSProtection = "1; mode=block" - } - if config.ContentTypeOptions == "" { - config.ContentTypeOptions = "nosniff" - } - - return func(c *gin.Context) { - // Thêm các security headers - if config.ContentSecurityPolicy != "" { - c.Writer.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy) - } - if config.CrossOriginResourcePolicy != "" { - c.Writer.Header().Set("Cross-Origin-Resource-Policy", config.CrossOriginResourcePolicy) - } - if config.CrossOriginOpenerPolicy != "" { - c.Writer.Header().Set("Cross-Origin-Opener-Policy", config.CrossOriginOpenerPolicy) - } - if config.CrossOriginEmbedderPolicy != "" { - c.Writer.Header().Set("Cross-Origin-Embedder-Policy", config.CrossOriginEmbedderPolicy) - } - if config.ReferrerPolicy != "" { - c.Writer.Header().Set("Referrer-Policy", config.ReferrerPolicy) - } - if config.FeaturePolicy != "" { - c.Writer.Header().Set("Feature-Policy", config.FeaturePolicy) - } - if config.FrameOptions != "" { - c.Writer.Header().Set("X-Frame-Options", config.FrameOptions) - } - if config.XSSProtection != "" { - c.Writer.Header().Set("X-XSS-Protection", config.XSSProtection) - } - if config.ContentTypeOptions != "" { - c.Writer.Header().Set("X-Content-Type-Options", config.ContentTypeOptions) - } - if config.StrictTransportSecurity != "" && strings.ToLower(c.Request.URL.Scheme) == "https" { - c.Writer.Header().Set("Strict-Transport-Security", config.StrictTransportSecurity) - } - if config.PermissionsPolicy != "" { - c.Writer.Header().Set("Permissions-Policy", config.PermissionsPolicy) - } - - c.Next() - } -}