package middlewares import ( "strings" "quyun/v2/app/errorx" "quyun/v2/app/services" "quyun/v2/database/models" "quyun/v2/pkg/consts" "quyun/v2/providers/jwt" "github.com/gofiber/fiber/v3" ) func shouldSkipTenantJWTAuth(path string) bool { // Public read endpoints allow anonymous access (optional JWT). if strings.Contains(path, "/v1/public/") { return true } // Media play is token-based, no JWT required. if strings.Contains(path, "/v1/media/play") { return true } return false } func shouldSkipTenantRequireMember(path string) bool { // Public read endpoints allow anonymous access. if strings.Contains(path, "/v1/public/") { return true } // Join endpoints require JWT but not tenant membership. if strings.Contains(path, "/v1/join/") { return true } // Media play is token-based, no JWT required. if strings.Contains(path, "/v1/media/play") { return true } return false } func (f *Middlewares) TenantResolve(c fiber.Ctx) error { tenantCode := c.Params("tenantCode") if tenantCode == "" { return errorx.ErrMissingParameter.WithMsg("缺少 tenantCode") } tenantModel, err := services.Tenant.FindByCode(c, tenantCode) if err != nil { f.log.WithField("tenant_code", tenantCode).WithError(err).Warn("middlewares.tenant.resolve.failed") return err } f.log.WithFields(map[string]any{ "tenant_id": tenantModel.ID, "tenant_code": tenantCode, }).Info("middlewares.tenant.resolve.ok") c.Locals(consts.CtxKeyTenant, tenantModel) return c.Next() } func (f *Middlewares) TenantAuth(c fiber.Ctx) error { if shouldSkipTenantJWTAuth(c.Path()) { f.log.Debug("middlewares.tenant.auth.skipped") return c.Next() } authHeader := c.Get(jwt.HttpHeader) if authHeader == "" { f.log.Info("middlewares.tenant.auth.missing_token") return errorx.ErrTokenMissing } claims, err := f.jwt.Parse(authHeader) if err != nil { f.log.WithError(err).Warn("middlewares.tenant.auth.invalid_token") switch err { case jwt.TokenExpired: return errorx.ErrTokenExpired case jwt.TokenMalformed, jwt.TokenNotValidYet, jwt.TokenInvalid: return errorx.ErrTokenInvalid default: return errorx.ErrTokenInvalid } } if claims.UserID == 0 { f.log.Warn("middlewares.tenant.auth.missing_user_id") return errorx.ErrTokenInvalid } f.log.WithFields(map[string]any{ "user_id": claims.UserID, }).Info("middlewares.tenant.auth.ok") c.Locals(consts.CtxKeyClaims, claims) return c.Next() } // TenantOptionalAuth 在 token 存在时解析并写入 claims,但允许无 token 的请求继续。 // 用于“公开只读”类接口:可匿名访问,但若携带 token 则可以得到更准确的 has_access 等判断。 func (f *Middlewares) TenantOptionalAuth(c fiber.Ctx) error { authHeader := c.Get(jwt.HttpHeader) if authHeader == "" { f.log.Debug("middlewares.tenant.optional_auth.no_token") return c.Next() } claims, err := f.jwt.Parse(authHeader) if err != nil { f.log.WithError(err).Warn("middlewares.tenant.optional_auth.invalid_token") switch err { case jwt.TokenExpired: return errorx.ErrTokenExpired case jwt.TokenMalformed, jwt.TokenNotValidYet, jwt.TokenInvalid: return errorx.ErrTokenInvalid default: return errorx.ErrTokenInvalid } } if claims.UserID == 0 { f.log.Warn("middlewares.tenant.optional_auth.missing_user_id") return errorx.ErrTokenInvalid } f.log.WithFields(map[string]any{ "user_id": claims.UserID, }).Debug("middlewares.tenant.optional_auth.ok") c.Locals(consts.CtxKeyClaims, claims) return c.Next() } func (f *Middlewares) TenantRequireMember(c fiber.Ctx) error { if shouldSkipTenantRequireMember(c.Path()) { f.log.Debug("middlewares.tenant.require_member.skipped") return c.Next() } tenantModel, ok := c.Locals(consts.CtxKeyTenant).(*models.Tenant) if !ok || tenantModel == nil { f.log.Error("middlewares.tenant.require_member.missing_tenant_context") return errorx.ErrInternalError.WithMsg("tenant context missing") } claims, ok := c.Locals(consts.CtxKeyClaims).(*jwt.Claims) if !ok || claims == nil { f.log.Error("middlewares.tenant.require_member.missing_claims_context") return errorx.ErrInternalError.WithMsg("claims context missing") } tenantUser, err := services.Tenant.FindTenantUser(c, tenantModel.ID, claims.UserID) if err != nil { f.log.WithFields(map[string]any{ "tenant_id": tenantModel.ID, "user_id": claims.UserID, }).WithError(err).Warn("middlewares.tenant.require_member.denied") return errorx.ErrPermissionDenied.WithMsg("不属于该租户") } userModel, err := services.User.FindByID(c, claims.UserID) if err != nil { f.log.WithField("user_id", claims.UserID).WithError(err).Warn("middlewares.tenant.require_member.load_user_failed") return err } f.log.WithFields(map[string]any{ "tenant_id": tenantModel.ID, "user_id": claims.UserID, }).Info("middlewares.tenant.require_member.ok") c.Locals(consts.CtxKeyTenantUser, tenantUser) c.Locals(consts.CtxKeyUser, userModel) return c.Next() }