98 lines
2.7 KiB
Go
98 lines
2.7 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"caorushizi.cn/mediago/internal/api/dto"
|
|
"caorushizi.cn/mediago/internal/api/handler"
|
|
"caorushizi.cn/mediago/internal/i18n"
|
|
"caorushizi.cn/mediago/internal/logger"
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// AuthMiddleware creates a Gin middleware that validates apiKey from the
|
|
// Authorization header (Bearer token) or query parameter (for SSE).
|
|
// Whitelisted paths bypass authentication.
|
|
func AuthMiddleware(confStore handler.ConfigStore) gin.HandlerFunc {
|
|
whitelist := map[string]bool{
|
|
"/healthy": true,
|
|
"/api/auth/setup": true,
|
|
"/api/auth/signin": true,
|
|
"/api/auth/status": true,
|
|
}
|
|
|
|
return func(c *gin.Context) {
|
|
path := c.Request.URL.Path
|
|
|
|
// Whitelist check
|
|
if whitelist[path] || strings.HasPrefix(path, "/swagger/") ||
|
|
strings.HasPrefix(path, "/player") ||
|
|
strings.HasPrefix(path, "/api/v1/") ||
|
|
strings.HasPrefix(path, "/videos/") ||
|
|
strings.HasPrefix(path, "/assets/") ||
|
|
path == "/favicon.ico" ||
|
|
path == "/" {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// SPA frontend routes (non-API paths without dots are likely client-side routes)
|
|
if !strings.HasPrefix(path, "/api/") && !strings.Contains(path, ".") {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Get stored apiKey from config
|
|
storedKey, _ := confStore.Get("apiKey").(string)
|
|
if storedKey == "" {
|
|
// No apiKey configured, auth not set up yet — allow access
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Extract token from Authorization header or query param
|
|
token := extractToken(c)
|
|
if token == "" {
|
|
logger.Warn("Auth: missing token", zap.String("path", path), zap.String("clientIP", c.ClientIP()))
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, dto.ErrorResponse{
|
|
Success: false,
|
|
Code: http.StatusUnauthorized,
|
|
Message: i18n.T(c, i18n.MsgUnauthorized),
|
|
})
|
|
return
|
|
}
|
|
|
|
if token != storedKey {
|
|
logger.Warn("Auth: invalid token", zap.String("path", path), zap.String("clientIP", c.ClientIP()))
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, dto.ErrorResponse{
|
|
Success: false,
|
|
Code: http.StatusUnauthorized,
|
|
Message: i18n.T(c, i18n.MsgUnauthorized),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// extractToken gets the API key from request headers or query param.
|
|
// Priority: X-API-Key header > Authorization: Bearer header > ?token= query param.
|
|
func extractToken(c *gin.Context) string {
|
|
// Try X-API-Key header first (used by frontend http client)
|
|
if key := c.GetHeader("X-API-Key"); key != "" {
|
|
return key
|
|
}
|
|
|
|
// Try Authorization header
|
|
auth := c.GetHeader("Authorization")
|
|
if strings.HasPrefix(auth, "Bearer ") {
|
|
return strings.TrimPrefix(auth, "Bearer ")
|
|
}
|
|
|
|
// Fallback to query param (for SSE EventSource which can't set headers)
|
|
return c.Query("token")
|
|
}
|