draft grpc

This commit is contained in:
2026-03-13 02:17:18 +00:00
parent ea2edbb9e0
commit 91e5e3542b
116 changed files with 44505 additions and 558 deletions

View File

@@ -1,155 +1,52 @@
//go:build ignore
// +build ignore
package middleware
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"stream.api/internal/config"
"stream.api/internal/database/query"
"stream.api/pkg/cache"
"stream.api/pkg/logger"
"stream.api/pkg/response"
"stream.api/pkg/token"
)
const (
CookieName = "auth_token" // Backward compatibility if needed, but we use access_token now
)
type AuthMiddleware struct {
cache cache.Cache
token token.Provider
authenticator *Authenticator
}
func NewAuthMiddleware(c cache.Cache, t token.Provider, cfg *config.Config) *AuthMiddleware {
func NewAuthMiddleware(c cache.Cache, t token.Provider, _ *config.Config, db *gorm.DB, l logger.Logger) *AuthMiddleware {
return &AuthMiddleware{
cache: c,
token: t,
authenticator: NewAuthenticator(c, t, db, l),
}
}
func (m *AuthMiddleware) Handle() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := c.Request.Context()
var userID string
var claims *token.Claims
var err error
// 1. Try Access Token (Header or Cookie)
var accessToken string
authHeader := c.GetHeader("Authorization")
if len(authHeader) > 7 && strings.ToUpper(authHeader[0:7]) == "BEARER " {
accessToken = authHeader[7:]
}
if accessToken == "" {
cookie, err := c.Cookie("access_token")
if err == nil {
accessToken = cookie
}
}
if accessToken != "" {
claims, err = m.token.ParseToken(accessToken)
if err == nil {
userID = claims.UserID
}
}
// 2. If Access Token invalid/missing, Try Refresh Token
if userID == "" {
refreshToken, errRefresh := c.Cookie("refresh_token")
if errRefresh != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized: No token")
return
}
// Validate Refresh Token (JWT signature)
// Problem regarding pkg/token parsing: Generates MapClaims but ParseToken expects Claims struct.
// Let's rely on Redis mostly, or assume ParseToken *fails* if claims mismatch, which is fine.
// Better: Assume we can parse at least standard claims or check Redis directly?
// But we need the UUID inside the token to check Redis key `refresh_uuid:{uuid}`.
// WORKAROUND: In pkg/token/jwt.go we defined `rtClaims["refresh_uuid"]`.
// `ParseToken` attempts to map to `Claims`. It likely won't map unknown fields but `Valid` might be true if struct has RegisteredClaims?
// Actually `jwt.ParseWithClaims` will succeed if signature matches, even if some fields are missing in struct.
// But we need `refresh_uuid`!
//
// We MUST parse as MapClaims here or update pkg/token.
// For this execution, I will parse as MapClaims locally to unblock.
// Validate Refresh Token (JWT signature)
// Parse using local helper to check refresh_uuid claim
// We can use m.token.ParseMapToken which we implemented
rtClaims, err := m.token.ParseMapToken(refreshToken)
if err != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized: Invalid refresh token")
return
}
refreshUUID, ok := rtClaims["refresh_uuid"].(string)
if !ok {
response.Error(c, http.StatusUnauthorized, "Unauthorized: Invalid refresh token claim")
return
}
// Check Redis
redisKey := "refresh_uuid:" + refreshUUID
userIDStr, err := m.cache.Get(ctx, redisKey)
if err != nil {
// Assuming Get returns error on miss or failure
response.Error(c, http.StatusUnauthorized, "Unauthorized: Session expired or invalid")
return
}
userID = userIDStr
// Refresh Success: Generate NEW Pair (Rotational or just new Access?)
// Let's just issue new Access Token. Refresh Token rotation is safer but complex (need to update Redis key).
// We will just issue new Access Token.
// Need User Role/Email for Access Token claims.
u := query.User
userObj, err := u.WithContext(ctx).Where(u.ID.Eq(userID)).First()
if err != nil {
response.Error(c, http.StatusUnauthorized, "User not found")
return
}
// Calling pkg/token.GenerateTokenPair generates BOTH.
// If we want new Access we can regenerate pair and update tokens.
// Updating refresh token extends session (Slide expiration).
newTd, err := m.token.GenerateTokenPair(userID, userObj.Email, *userObj.Role)
if err == nil {
// Delete old refresh token from Redis?
m.cache.Del(ctx, redisKey)
// Set new
m.cache.Set(ctx, "refresh_uuid:"+newTd.RefreshUUID, userID, time.Until(time.Unix(newTd.RtExpires, 0)))
// Set Cookies
c.SetCookie("access_token", newTd.AccessToken, int(newTd.AtExpires-time.Now().Unix()), "/", "", false, true)
c.SetCookie("refresh_token", newTd.RefreshToken, int(newTd.RtExpires-time.Now().Unix()), "/", "", false, true)
}
}
// 3. User Lookup / Block Check
u := query.User
user, err := u.WithContext(ctx).Where(u.ID.Eq(userID)).First()
result, err := m.authenticator.Authenticate(c.Request.Context(), AuthRequest{
Authorization: c.GetHeader("Authorization"),
CookieHeader: c.GetHeader("Cookie"),
})
if err != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized: User not found")
authErr, ok := err.(*AuthError)
if !ok {
response.Error(c, http.StatusInternalServerError, "Authentication failed")
return
}
response.Error(c, authErr.StatusCode, authErr.Message)
return
}
if user.Role != nil && strings.ToLower(*user.Role) == "block" {
response.Error(c, http.StatusForbidden, "Forbidden: User is blocked")
return
for _, cookie := range result.SetCookies {
c.Header("Set-Cookie", cookie)
}
c.Set("userID", user.ID)
c.Set("user", user)
c.Set("userID", result.UserID)
c.Set("user", result.User)
c.Next()
}
}
// Helper to parse generic claims
// Removed parseMapToken as it is now in TokenProvider interface