[BACKEND] Initialize Go backend structure and dependencies (#23)
This commit is contained in:
68
server/middleware/auth.go
Normal file
68
server/middleware/auth.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/config"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
)
|
||||
|
||||
// Claims represents the JWT payload.
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// Auth returns a Gin middleware that validates JWT bearer tokens.
|
||||
// When no database is connected (unmanaged mode), all requests
|
||||
// are allowed through without authentication.
|
||||
func Auth(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Skip auth when running without a database (unmanaged mode)
|
||||
if !database.IsConnected() {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
header := c.GetHeader("Authorization")
|
||||
if header == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "missing authorization header",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(header, "Bearer ")
|
||||
if tokenString == header {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "invalid authorization format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return []byte(cfg.JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "invalid or expired token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Store claims in context for downstream handlers
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("email", claims.Email)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
100
server/middleware/cors.go
Normal file
100
server/middleware/cors.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORSConfig holds the configuration for the CORS middleware
|
||||
type CORSConfig struct {
|
||||
AllowedOrigins []string
|
||||
AllowedMethods []string
|
||||
AllowedHeaders []string
|
||||
AllowCredentials bool
|
||||
MaxAge int
|
||||
}
|
||||
|
||||
// DefaultCORSConfig returns the default CORS configuration
|
||||
func DefaultCORSConfig() *CORSConfig {
|
||||
return &CORSConfig{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Requested-With", "Accept", "Origin"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 86400, // 24 hours
|
||||
}
|
||||
}
|
||||
|
||||
// CORS returns a Gin middleware handler for CORS (Cross-Origin Resource Sharing)
|
||||
func CORS() gin.HandlerFunc {
|
||||
return CORSWithConfig(DefaultCORSConfig())
|
||||
}
|
||||
|
||||
// CORSWithConfig returns a Gin middleware handler for CORS with custom configuration
|
||||
func CORSWithConfig(config *CORSConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
|
||||
// Check if the origin is allowed
|
||||
if len(config.AllowedOrigins) > 0 && config.AllowedOrigins[0] != "*" {
|
||||
allowed := false
|
||||
for _, allowedOrigin := range config.AllowedOrigins {
|
||||
if origin == allowedOrigin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
c.AbortWithStatusJSON(403, gin.H{
|
||||
"error": "origin not allowed",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set CORS headers
|
||||
if len(config.AllowedOrigins) > 0 {
|
||||
if config.AllowedOrigins[0] == "*" {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Allow-Methods", joinStringSlice(config.AllowedMethods, ", "))
|
||||
c.Header("Access-Control-Allow-Headers", joinStringSlice(config.AllowedHeaders, ", "))
|
||||
|
||||
if config.AllowCredentials {
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Max-Age", stringFromInt(config.MaxAge))
|
||||
|
||||
// Handle preflight requests
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to join a slice of strings
|
||||
func joinStringSlice(slice []string, sep string) string {
|
||||
if len(slice) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := slice[0]
|
||||
for i := 1; i < len(slice); i++ {
|
||||
result += sep + slice[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper function to convert int to string
|
||||
func stringFromInt(n int) string {
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
return string(rune('0'+n%10)) + stringFromInt(n/10)
|
||||
}
|
||||
114
server/middleware/errors.go
Normal file
114
server/middleware/errors.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ErrorHandlerConfig holds the configuration for the error handler middleware
|
||||
type ErrorHandlerConfig struct {
|
||||
TimeFormat string
|
||||
TimeZone string
|
||||
}
|
||||
|
||||
// DefaultErrorHandlerConfig returns the default error handler configuration
|
||||
func DefaultErrorHandlerConfig() *ErrorHandlerConfig {
|
||||
return &ErrorHandlerConfig{
|
||||
TimeFormat: "2006/01/02 - 15:04:05",
|
||||
TimeZone: "UTC",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler returns a Gin middleware handler for centralized error handling
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return ErrorHandlerWithConfig(DefaultErrorHandlerConfig())
|
||||
}
|
||||
|
||||
// ErrorHandlerWithConfig returns a Gin middleware handler for centralized error handling with custom configuration
|
||||
func ErrorHandlerWithConfig(config *ErrorHandlerConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// Check if there were any errors
|
||||
if len(c.Errors) > 0 {
|
||||
err := c.Errors.Last()
|
||||
|
||||
// Determine the status code
|
||||
statusCode := http.StatusInternalServerError
|
||||
if gin.Mode() != gin.ReleaseMode {
|
||||
// In development mode, return detailed error information
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "internal server error",
|
||||
"message": err.Error(),
|
||||
"details": c.Errors,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// In production mode, return a generic error message
|
||||
c.JSON(statusCode, gin.H{
|
||||
"error": "internal server error",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorResponse represents a standard error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// NewErrorResponse creates a new error response
|
||||
func NewErrorResponse(error string, code string, details map[string]string) *ErrorResponse {
|
||||
return &ErrorResponse{
|
||||
Error: error,
|
||||
Code: code,
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
|
||||
// BadRequest creates a 400 Bad Request response
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
|
||||
// Unauthorized creates a 401 Unauthorized response
|
||||
func Unauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
|
||||
// Forbidden creates a 403 Forbidden response
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
|
||||
// NotFound creates a 404 Not Found response
|
||||
func NotFound(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalServerError creates a 500 Internal Server Error response
|
||||
func InternalServerError(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
|
||||
// ValidationError creates a response for validation errors
|
||||
func ValidationError(c *gin.Context, errors map[string]string) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "validation failed",
|
||||
"details": errors,
|
||||
})
|
||||
}
|
||||
130
server/middleware/logging.go
Normal file
130
server/middleware/logging.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RequestLoggerConfig holds the configuration for the request logger
|
||||
type RequestLoggerConfig struct {
|
||||
SkipPaths []string
|
||||
Logger *log.Logger
|
||||
TimeFormat string
|
||||
TimeZone string
|
||||
}
|
||||
|
||||
// DefaultRequestLoggerConfig returns the default request logger configuration
|
||||
func DefaultRequestLoggerConfig() *RequestLoggerConfig {
|
||||
return &RequestLoggerConfig{
|
||||
SkipPaths: []string{"/health", "/ready", "/metrics"},
|
||||
Logger: log.Default(),
|
||||
TimeFormat: "2006/01/02 - 15:04:05",
|
||||
TimeZone: "UTC",
|
||||
}
|
||||
}
|
||||
|
||||
// Logger returns a Gin middleware handler for logging requests
|
||||
func Logger() gin.HandlerFunc {
|
||||
return LoggerWithConfig(DefaultRequestLoggerConfig())
|
||||
}
|
||||
|
||||
// LoggerWithConfig returns a Gin middleware handler for logging requests with custom configuration
|
||||
func LoggerWithConfig(config *RequestLoggerConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Skip logging for certain paths
|
||||
path := c.Request.URL.Path
|
||||
skip := false
|
||||
for _, skipPath := range config.SkipPaths {
|
||||
if path == skipPath {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
if skip {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate latency
|
||||
latency := time.Since(start)
|
||||
|
||||
// Get the client IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// Get the request method
|
||||
method := c.Request.Method
|
||||
|
||||
// Get the status code
|
||||
statusCode := c.Writer.Status()
|
||||
|
||||
// Log the request
|
||||
if raw != "" {
|
||||
raw = "?" + raw
|
||||
}
|
||||
|
||||
config.Logger.Printf(
|
||||
"[%s] %s %s%s | %d | %v | %s",
|
||||
clientIP,
|
||||
method,
|
||||
path,
|
||||
raw,
|
||||
statusCode,
|
||||
latency,
|
||||
c.Errors.ByType(gin.ErrorTypePrivate).String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery returns a Gin middleware handler for recovering from panics
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return RecoveryWithConfig(DefaultRecoveryConfig())
|
||||
}
|
||||
|
||||
// RecoveryConfig holds the configuration for the recovery middleware
|
||||
type RecoveryConfig struct {
|
||||
Logger *log.Logger
|
||||
TimeFormat string
|
||||
TimeZone string
|
||||
SkipStackTrace bool
|
||||
StackTraceSize int
|
||||
StackAll bool
|
||||
}
|
||||
|
||||
// DefaultRecoveryConfig returns the default recovery configuration
|
||||
func DefaultRecoveryConfig() *RecoveryConfig {
|
||||
return &RecoveryConfig{
|
||||
Logger: log.Default(),
|
||||
TimeFormat: "2006/01/02 - 15:04:05",
|
||||
TimeZone: "UTC",
|
||||
SkipStackTrace: false,
|
||||
StackTraceSize: 1024,
|
||||
StackAll: false,
|
||||
}
|
||||
}
|
||||
|
||||
// RecoveryWithConfig returns a Gin middleware handler for recovering from panics with custom configuration
|
||||
func RecoveryWithConfig(config *RecoveryConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// Log the panic
|
||||
config.Logger.Printf("[PANIC] %v", err)
|
||||
|
||||
// Abort with a 500 error
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "internal server error",
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user