Files
GopherGate/internal/middleware/auth.go
T
hobokenchicken 0decc63e8c
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
feat: add token caching middleware and max history messages config
2026-07-21 17:30:22 +00:00

134 lines
3.4 KiB
Go

package middleware
import (
"log"
"net/http"
"strings"
"sync"
"time"
"gophergate/internal/db"
"gophergate/internal/models"
"github.com/gin-gonic/gin"
)
type tokenCacheEntry struct {
clientID string
expiredAt time.Time
}
var (
tokenCache sync.Map // map[string]tokenCacheEntry
)
func AuthMiddleware(database *db.DB, requireAuth bool) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
// Fallback to checking "Authentication" header in case the client library used the wrong name
authHeader = c.GetHeader("Authentication")
}
if authHeader == "" {
if requireAuth {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"message": "Missing Authorization or Authentication header.",
"type": "invalid_request_error",
"param": nil,
"code": "401",
},
})
return
}
c.Next()
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == authHeader { // No "Bearer " prefix
if requireAuth {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"message": "Invalid authorization header format. Bearer token required.",
"type": "invalid_request_error",
"param": nil,
"code": "401",
},
})
return
}
c.Next()
return
}
// Try to resolve client from cache first
var clientID string
var dbErr error
now := time.Now()
if cached, ok := tokenCache.Load(token); ok {
entry := cached.(tokenCacheEntry)
if now.Before(entry.expiredAt) {
clientID = entry.clientID
}
}
// If cache miss (or expired), query database
if clientID == "" {
var fetchedID string
dbErr = database.Get(&fetchedID, "SELECT client_id FROM client_tokens WHERE token = ? AND is_active = 1", token)
if dbErr == nil {
clientID = fetchedID
// Cache valid token for 10 seconds
tokenCache.Store(token, tokenCacheEntry{
clientID: clientID,
expiredAt: now.Add(10 * time.Second),
})
} else {
// If error (invalid/inactive token), cache negative result for 2 seconds
// to avoid hammering SQLite on repeated invalid requests
tokenCache.Store(token, tokenCacheEntry{
clientID: "",
expiredAt: now.Add(2 * time.Second),
})
}
}
if clientID != "" {
c.Set("auth", models.AuthInfo{
Token: token,
ClientID: clientID,
})
// Update last_used_at asynchronously so that database locks or write delays
// do not block or fail the client's request authentication.
go func(t string) {
if _, updateErr := database.Exec("UPDATE client_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE token = ?", t); updateErr != nil {
log.Printf("Warning: failed to update client token last_used_at: %v", updateErr)
}
}(token)
c.Next()
} else {
maskedToken := "••••"
if len(token) > 8 {
maskedToken = token[:3] + "••••" + token[len(token)-4:]
}
if dbErr != nil {
log.Printf("Token not found, inactive or error in DB: %s (err: %v)", maskedToken, dbErr)
} else {
log.Printf("Token not found or inactive: %s", maskedToken)
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"message": "Invalid or inactive client token.",
"type": "invalid_request_error",
"param": nil,
"code": "401",
},
})
}
}
}