package middleware import ( "context" "net/http" "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/store" ) // CtxSpaceID 是当前活跃工作区(Space) id 在 gin.Context 的键(增量3 共享工作区)。 const CtxSpaceID = "space_id" // SpaceContext 解析当前用户在活跃租户内的活跃工作区并注入 space_id。 // 须挂在 Auth + TenantContext 之后(依赖 uid 与 tenant_id);未登录/无租户则跳过。 // 活跃空间无效(跨租户/已归档/非成员)时由 ActiveSpaceForUser 兜底回退个人空间。 func SpaceContext(db *store.Postgres) gin.HandlerFunc { return func(c *gin.Context) { uid, _ := c.Get(CtxUserID) id, _ := uid.(string) tid, _ := c.Get(CtxTenantID) tenant, _ := tid.(string) if id != "" && tenant != "" { if s, err := db.ActiveSpaceForUser(c.Request.Context(), tenant, id); err == nil && s != nil { c.Set(CtxSpaceID, s.ID) } } c.Next() } } // SpaceRoleResolver 解析用户在某空间的角色(*store.Postgres 天然满足)。抽接口便于单测门控。 type SpaceRoleResolver interface { SpaceMemberRole(ctx context.Context, spaceID, userID string) string } // RequireSpaceRole 要求当前用户在活跃空间中的角色不低于 minRole(按 store.RoleRank 阶梯)。 // 须挂在 RequireAuth + SpaceContext 之后。非成员/无空间 → rank 0 → 拦下。 // 用于共享工作区的写操作门控(建/存/删编排要 ≥member;viewer 只读)。 func RequireSpaceRole(db SpaceRoleResolver, minRole string) gin.HandlerFunc { min := store.RoleRank(minRole) return func(c *gin.Context) { uid, _ := c.Get(CtxUserID) id, _ := uid.(string) if id == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "需要登录"}) return } sid, _ := c.Get(CtxSpaceID) space, _ := sid.(string) if store.RoleRank(db.SpaceMemberRole(c.Request.Context(), space, id)) < min { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "当前空间角色为只读,无权执行该操作"}) return } c.Next() } }