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", }, }) } } }