134 lines
3.4 KiB
Go
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",
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|