Issue 4 jwt (#24)

This commit is contained in:
2026-02-15 23:41:48 +00:00
parent c75f976a2d
commit 8f10352e7d
9 changed files with 883 additions and 159 deletions

View File

@@ -11,10 +11,11 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/database"
)
// Claims represents the JWT payload.
// Claims represents the JWT payload. Must match handlers.Claims.
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
@@ -63,6 +64,22 @@ func Auth(cfg *config.Config) gin.HandlerFunc {
// Store claims in context for downstream handlers
c.Set("user_id", claims.UserID)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
c.Next()
}
}
}
// CORS returns a middleware that sets cross-origin headers.
// NOTE: If you have a separate cors.go, delete it — CORS lives here only.
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}

View File

@@ -1,100 +0,0 @@
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)
}

View File

@@ -0,0 +1,92 @@
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
type visitor struct {
tokens float64
lastSeen time.Time
}
// RateLimiter implements a per-IP token bucket rate limiter.
type RateLimiter struct {
mu sync.Mutex
visitors map[string]*visitor
rate float64 // tokens per second
burst int // max tokens
}
// NewRateLimiter creates a rate limiter.
// rate is requests per second, burst is max burst size.
func NewRateLimiter(rate float64, burst int) *RateLimiter {
rl := &RateLimiter{
visitors: make(map[string]*visitor),
rate: rate,
burst: burst,
}
// Cleanup stale entries every 5 minutes
go func() {
for {
time.Sleep(5 * time.Minute)
rl.cleanup()
}
}()
return rl
}
// Limit returns a Gin middleware that enforces the rate limit.
func (rl *RateLimiter) Limit() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
rl.mu.Lock()
v, exists := rl.visitors[ip]
now := time.Now()
if !exists {
v = &visitor{tokens: float64(rl.burst), lastSeen: now}
rl.visitors[ip] = v
}
// Refill tokens based on elapsed time
elapsed := now.Sub(v.lastSeen).Seconds()
v.tokens += elapsed * rl.rate
if v.tokens > float64(rl.burst) {
v.tokens = float64(rl.burst)
}
v.lastSeen = now
if v.tokens < 1 {
rl.mu.Unlock()
c.Header("Retry-After", "1")
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
})
return
}
v.tokens--
rl.mu.Unlock()
c.Next()
}
}
func (rl *RateLimiter) cleanup() {
rl.mu.Lock()
defer rl.mu.Unlock()
cutoff := time.Now().Add(-10 * time.Minute)
for ip, v := range rl.visitors {
if v.lastSeen.Before(cutoff) {
delete(rl.visitors, ip)
}
}
}