refactor: Flatten directory structure
Move project files from uzdb/ subdirectory to root directory for cleaner project structure.
Changes:
- Move frontend/ to root
- Move internal/ to root
- Move build/ to root
- Move all config files (go.mod, wails.json, etc.) to root
- Remove redundant uzdb/ subdirectory nesting
Project structure is now:
├── frontend/ # React application
├── internal/ # Go backend
├── build/ # Wails build assets
├── doc/ # Design documentation
├── main.go # Entry point
└── ...
🤖 Generated with Qoder
This commit is contained in:
101
internal/middleware/cors.go
Normal file
101
internal/middleware/cors.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"uzdb/internal/config"
|
||||
)
|
||||
|
||||
// CORSMiddleware returns a CORS middleware
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cfg := config.Get()
|
||||
|
||||
// Set CORS headers
|
||||
c.Header("Access-Control-Allow-Origin", getAllowedOrigin(c, cfg))
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-Request-ID")
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE")
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, X-Request-ID")
|
||||
c.Header("Access-Control-Max-Age", "43200") // 12 hours
|
||||
|
||||
// Handle preflight requests
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// getAllowedOrigin returns the allowed origin based on configuration
|
||||
func getAllowedOrigin(c *gin.Context, cfg *config.Config) string {
|
||||
origin := c.GetHeader("Origin")
|
||||
|
||||
// If no origin header, return empty (same-origin)
|
||||
if origin == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// In development mode, allow all origins
|
||||
if cfg.IsDevelopment() {
|
||||
return origin
|
||||
}
|
||||
|
||||
// In production, validate against allowed origins
|
||||
allowedOrigins := []string{
|
||||
"http://localhost:3000",
|
||||
"http://localhost:8080",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://127.0.0.1:8080",
|
||||
}
|
||||
|
||||
for _, allowed := range allowedOrigins {
|
||||
if origin == allowed {
|
||||
return origin
|
||||
}
|
||||
}
|
||||
|
||||
// Check for wildcard patterns
|
||||
for _, allowed := range allowedOrigins {
|
||||
if strings.HasSuffix(allowed, "*") {
|
||||
prefix := strings.TrimSuffix(allowed, "*")
|
||||
if strings.HasPrefix(origin, prefix) {
|
||||
return origin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to empty (deny) in production if not matched
|
||||
if cfg.IsProduction() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return origin
|
||||
}
|
||||
|
||||
// SecureHeadersMiddleware adds security-related headers
|
||||
func SecureHeadersMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Prevent MIME type sniffing
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// Enable XSS filter
|
||||
c.Header("X-XSS-Protection", "1; mode=block")
|
||||
|
||||
// Prevent clickjacking
|
||||
c.Header("X-Frame-Options", "DENY")
|
||||
|
||||
// Referrer policy
|
||||
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
// Content Security Policy (adjust as needed)
|
||||
c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
125
internal/middleware/error.go
Normal file
125
internal/middleware/error.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"uzdb/internal/config"
|
||||
"uzdb/internal/models"
|
||||
)
|
||||
|
||||
// RecoveryMiddleware returns a recovery middleware that handles panics
|
||||
func RecoveryMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger := config.GetLogger()
|
||||
|
||||
// Log the panic with stack trace
|
||||
logger.Error("panic recovered",
|
||||
zap.Any("error", err),
|
||||
zap.String("stack", string(debug.Stack())),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
)
|
||||
|
||||
// Send error response
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||
Error: "INTERNAL_ERROR",
|
||||
Message: "An unexpected error occurred",
|
||||
Timestamp: time.Now(),
|
||||
Path: c.Request.URL.Path,
|
||||
})
|
||||
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorMiddleware returns an error handling middleware
|
||||
func ErrorMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// Check if there are any errors
|
||||
if len(c.Errors) > 0 {
|
||||
handleErrors(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleErrors processes and formats errors
|
||||
func handleErrors(c *gin.Context) {
|
||||
logger := config.GetLogger()
|
||||
|
||||
for _, e := range c.Errors {
|
||||
err := e.Err
|
||||
|
||||
// Log the error
|
||||
logger.Error("request error",
|
||||
zap.String("error", err.Error()),
|
||||
zap.Any("type", e.Type),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
)
|
||||
|
||||
// Determine status code
|
||||
statusCode := http.StatusInternalServerError
|
||||
|
||||
// Map specific errors to status codes
|
||||
switch {
|
||||
case errors.Is(err, models.ErrNotFound):
|
||||
statusCode = http.StatusNotFound
|
||||
case errors.Is(err, models.ErrValidationFailed):
|
||||
statusCode = http.StatusBadRequest
|
||||
case errors.Is(err, models.ErrUnauthorized):
|
||||
statusCode = http.StatusUnauthorized
|
||||
case errors.Is(err, models.ErrForbidden):
|
||||
statusCode = http.StatusForbidden
|
||||
case errors.Is(err, models.ErrConnectionFailed):
|
||||
statusCode = http.StatusBadGateway
|
||||
}
|
||||
|
||||
// Send response if not already sent
|
||||
if !c.Writer.Written() {
|
||||
c.JSON(statusCode, models.ErrorResponse{
|
||||
Error: getErrorCode(err),
|
||||
Message: err.Error(),
|
||||
Timestamp: time.Now(),
|
||||
Path: c.Request.URL.Path,
|
||||
})
|
||||
}
|
||||
|
||||
// Only handle first error
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// getErrorCode maps errors to error codes
|
||||
func getErrorCode(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, models.ErrNotFound):
|
||||
return string(models.CodeNotFound)
|
||||
case errors.Is(err, models.ErrValidationFailed):
|
||||
return string(models.CodeValidation)
|
||||
case errors.Is(err, models.ErrUnauthorized):
|
||||
return string(models.CodeUnauthorized)
|
||||
case errors.Is(err, models.ErrForbidden):
|
||||
return string(models.CodeForbidden)
|
||||
case errors.Is(err, models.ErrConnectionFailed):
|
||||
return string(models.CodeConnection)
|
||||
case errors.Is(err, models.ErrQueryFailed):
|
||||
return string(models.CodeQuery)
|
||||
case errors.Is(err, models.ErrEncryptionFailed):
|
||||
return string(models.CodeEncryption)
|
||||
default:
|
||||
return string(models.CodeInternal)
|
||||
}
|
||||
}
|
||||
98
internal/middleware/logger.go
Normal file
98
internal/middleware/logger.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"uzdb/internal/config"
|
||||
)
|
||||
|
||||
// LoggerMiddleware returns a logging middleware using Zap
|
||||
func LoggerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
logger := config.GetLogger()
|
||||
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Calculate duration
|
||||
duration := time.Since(start)
|
||||
|
||||
// Get client IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// Get method
|
||||
method := c.Request.Method
|
||||
|
||||
// Get path
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Get status code
|
||||
statusCode := c.Writer.Status()
|
||||
|
||||
// Get body size
|
||||
bodySize := c.Writer.Size()
|
||||
|
||||
// Determine log level based on status code
|
||||
var level zapcore.Level
|
||||
switch {
|
||||
case statusCode >= 500:
|
||||
level = zapcore.ErrorLevel
|
||||
case statusCode >= 400:
|
||||
level = zapcore.WarnLevel
|
||||
default:
|
||||
level = zapcore.InfoLevel
|
||||
}
|
||||
|
||||
// Create fields
|
||||
fields := []zap.Field{
|
||||
zap.Int("status", statusCode),
|
||||
zap.String("method", method),
|
||||
zap.String("path", path),
|
||||
zap.String("ip", clientIP),
|
||||
zap.Duration("duration", duration),
|
||||
zap.Int("body_size", bodySize),
|
||||
}
|
||||
|
||||
// Add error message if any
|
||||
if len(c.Errors) > 0 {
|
||||
fields = append(fields, zap.String("error", c.Errors.String()))
|
||||
}
|
||||
|
||||
// Log the request
|
||||
logger.Log(level, "HTTP request", fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// RequestIDMiddleware adds a request ID to each request
|
||||
func RequestIDMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Try to get request ID from header
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
|
||||
// Generate if not present
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
|
||||
// Set request ID in context
|
||||
c.Set("request_id", requestID)
|
||||
|
||||
// Add to response header
|
||||
c.Header("X-Request-ID", requestID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID generates a simple request ID
|
||||
func generateRequestID() string {
|
||||
return time.Now().Format("20060102150405") + "-" +
|
||||
time.Now().Format("000000.000000")[7:]
|
||||
}
|
||||
Reference in New Issue
Block a user