feat(gateway): 多租户增量2 —— 租户切换 + 每租户共享计费开关
用户可切活跃租户,消耗按"计费租户"扣(数据仍落活跃租户/工作区): - User.active_tenant_id(切换持久化)+ Tenant.shared_billing(每租户开关,默认关)。 - 中间件 ActiveTenantForUser:active_tenant_id 若有效(仍是成员)则用之,否则默认租户。 - 计费目标解析 ResolveBillingTenantID:本人是活跃租户 owner 或该租户 shared_billing 开 → 记活跃租户;否则记本人个人租户(各付各的)。SubmitTask 的硬拦截 + 用量都按它走。 - 接口:GET /me/tenants、POST /me/tenant(切换)、PUT /admin/tenants/:id/shared-billing; /tenants/current 改显"可花余额"(计费租户) + billing_shared 标记。 live 验证(demoB 是公司A 成员、个人租户=公司B):切到公司A 后—— shared OFF:任务落公司A 工作区、用量扣公司B(个人),公司A 不变; shared ON:用量扣公司A(共享),公司B 不变;/tenants/current 余额随之在 6.48↔15.92 切换。全过。 前端切换器 + admin 开关见后续。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -323,6 +323,22 @@ func (h *Handler) AdminCreateTenant(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"tenant": t})
|
c.JSON(http.StatusOK, gin.H{"tenant": t})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminSetSharedBilling: PUT /api/v1/admin/tenants/:id/shared-billing {shared_billing} —— 设共享计费开关。
|
||||||
|
func (h *Handler) AdminSetSharedBilling(c *gin.Context) {
|
||||||
|
var b struct {
|
||||||
|
SharedBilling bool `json:"shared_billing"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&b); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.db.SetSharedBilling(c.Request.Context(), c.Param("id"), b.SharedBilling); err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
// AdminMembers: GET /api/v1/admin/tenants/:id/members —— 某租户成员列表。
|
// AdminMembers: GET /api/v1/admin/tenants/:id/members —— 某租户成员列表。
|
||||||
func (h *Handler) AdminMembers(c *gin.Context) {
|
func (h *Handler) AdminMembers(c *gin.Context) {
|
||||||
rows, err := h.db.ListMembers(c.Request.Context(), c.Param("id"))
|
rows, err := h.db.ListMembers(c.Request.Context(), c.Param("id"))
|
||||||
|
|||||||
@@ -56,9 +56,12 @@ func (h *Handler) SubmitTask(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 积分硬拦截(默认关;开关 credit_enforce):租户积分余额 ≤0 则拒绝新任务,提示充值。
|
// 计费目标:数据落在活跃租户(工作区),但消耗记到"计费租户"——owner/共享计费→活跃租户,
|
||||||
if tid := tenantID(c); tid != "" && h.db.CreditEnforceEnabled(c.Request.Context()) {
|
// 否则→本人个人租户(各付各的)。硬拦截与用量都按计费租户走。
|
||||||
if h.db.TenantBalance(c.Request.Context(), tid) <= 0 {
|
billingTenant := h.db.ResolveBillingTenantID(c.Request.Context(), userID(c), tenantID(c))
|
||||||
|
// 积分硬拦截(默认关;开关 credit_enforce):计费租户积分余额 ≤0 则拒绝新任务,提示充值。
|
||||||
|
if billingTenant != "" && h.db.CreditEnforceEnabled(c.Request.Context()) {
|
||||||
|
if h.db.TenantBalance(c.Request.Context(), billingTenant) <= 0 {
|
||||||
c.JSON(http.StatusPaymentRequired, gin.H{"error": "租户积分余额不足,请充值后再试", "balance_micro": 0})
|
c.JSON(http.StatusPaymentRequired, gin.H{"error": "租户积分余额不足,请充值后再试", "balance_micro": 0})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -66,7 +69,7 @@ func (h *Handler) SubmitTask(c *gin.Context) {
|
|||||||
// 附上用户标识(召回偏好记忆)与会话标识(召回短期多轮历史)。
|
// 附上用户标识(召回偏好记忆)与会话标识(召回短期多轮历史)。
|
||||||
// 真实场景由鉴权/会话中间件注入;此处用请求头,缺省匿名/默认会话。
|
// 真实场景由鉴权/会话中间件注入;此处用请求头,缺省匿名/默认会话。
|
||||||
task.Meta[contract.MetaUserID] = userID(c)
|
task.Meta[contract.MetaUserID] = userID(c)
|
||||||
task.Meta[contract.MetaTenantID] = tenantID(c)
|
task.Meta[contract.MetaTenantID] = billingTenant // 用量按计费租户扣(≠活跃租户时即"不共享")
|
||||||
task.Meta[contract.MetaSessionID] = sessionID(c)
|
task.Meta[contract.MetaSessionID] = sessionID(c)
|
||||||
// 输入护栏灰区升级:Tier1(中间件)判为疑似的输入打标,Dispatcher 执行前调 LLM 分类器裁决。
|
// 输入护栏灰区升级:Tier1(中间件)判为疑似的输入打标,Dispatcher 执行前调 LLM 分类器裁决。
|
||||||
if c.GetBool("guardrail_suspect") {
|
if c.GetBool("guardrail_suspect") {
|
||||||
@@ -536,20 +539,51 @@ func (h *Handler) TenantCurrent(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"tenant": nil})
|
c.JSON(http.StatusOK, gin.H{"tenant": nil})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t, _ := h.db.GetTenant(c.Request.Context(), tid)
|
ctx := c.Request.Context()
|
||||||
|
uid := userID(c)
|
||||||
|
t, _ := h.db.GetTenant(ctx, tid)
|
||||||
if t == nil {
|
if t == nil {
|
||||||
c.JSON(http.StatusOK, gin.H{"tenant": nil})
|
c.JSON(http.StatusOK, gin.H{"tenant": nil})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 余额显"可花的那本账"(计费租户):owner/共享→活跃租户;否则→个人租户。
|
||||||
|
billing := h.db.ResolveBillingTenantID(ctx, uid, tid)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"tenant": gin.H{"id": t.ID, "name": t.Name, "slug": t.Slug, "plan": t.Plan, "status": t.Status},
|
"tenant": gin.H{"id": t.ID, "name": t.Name, "slug": t.Slug, "plan": t.Plan, "status": t.Status, "shared_billing": t.SharedBilling},
|
||||||
"role": h.db.MemberRole(c.Request.Context(), tid, userID(c)),
|
"role": h.db.MemberRole(ctx, tid, uid),
|
||||||
// 计费可见性:用户自己租户的积分余额 + 平台是否开了硬拦截(桌面端据此显余额/提示充值)。
|
// 计费可见性:可花余额(计费租户)+ 消耗是否记本租户 + 硬拦截开关(桌面端据此显余额/提示充值)。
|
||||||
"credit_balance_micro": t.CreditBalanceMicro,
|
"credit_balance_micro": h.db.TenantBalance(ctx, billing),
|
||||||
"credit_enforce": h.db.CreditEnforceEnabled(c.Request.Context()),
|
"billing_shared": billing == tid,
|
||||||
|
"credit_enforce": h.db.CreditEnforceEnabled(ctx),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MyTenantsList: GET /api/v1/me/tenants —— 我所属的全部租户(供切换)+ 当前活跃租户 id。
|
||||||
|
func (h *Handler) MyTenantsList(c *gin.Context) {
|
||||||
|
rows, err := h.db.MyTenants(c.Request.Context(), userID(c))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"tenants": rows, "active": tenantID(c)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchTenant: POST /api/v1/me/tenant {tenant_id} —— 切换当前活跃租户(须为其成员)。
|
||||||
|
func (h *Handler) SwitchTenant(c *gin.Context) {
|
||||||
|
var b struct {
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&b); err != nil || b.TenantID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant_id 必填"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.db.SetActiveTenant(c.Request.Context(), userID(c), b.TenantID); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "active": b.TenantID})
|
||||||
|
}
|
||||||
|
|
||||||
// MyUsage: GET /api/v1/me/usage?days= —— 当前用户自己租户的用量明细(余额 + 按天趋势 + 最近消耗)。
|
// MyUsage: GET /api/v1/me/usage?days= —— 当前用户自己租户的用量明细(余额 + 按天趋势 + 最近消耗)。
|
||||||
// 面向用户口径(非 admin):只看自己租户,受插件按请求 ctx 租户自动隔离。
|
// 面向用户口径(非 admin):只看自己租户,受插件按请求 ctx 租户自动隔离。
|
||||||
func (h *Handler) MyUsage(c *gin.Context) {
|
func (h *Handler) MyUsage(c *gin.Context) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TenantContext(db *store.Postgres) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if v, ok := c.Get(CtxUserID); ok {
|
if v, ok := c.Get(CtxUserID); ok {
|
||||||
if uid, _ := v.(string); uid != "" {
|
if uid, _ := v.(string); uid != "" {
|
||||||
if t, err := db.EnsureDefaultTenant(c.Request.Context(), uid, ""); err == nil && t != nil {
|
if t, err := db.ActiveTenantForUser(c.Request.Context(), uid); err == nil && t != nil {
|
||||||
c.Set(CtxTenantID, t.ID)
|
c.Set(CtxTenantID, t.ID)
|
||||||
// 注入请求 context:store 的 gorm 租户插件据此对受租户表自动加 tenant_id 过滤/填充。
|
// 注入请求 context:store 的 gorm 租户插件据此对受租户表自动加 tenant_id 过滤/填充。
|
||||||
// handler 须用 c.Request.Context() 调 store(现有代码已如此),隔离才会生效。
|
// handler 须用 c.Request.Context() 调 store(现有代码已如此),隔离才会生效。
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
|||||||
p.POST("/tasks", h.SubmitTask) // 解析 DSL 并 Publish 到 NATS(带已验证 uid)
|
p.POST("/tasks", h.SubmitTask) // 解析 DSL 并 Publish 到 NATS(带已验证 uid)
|
||||||
p.GET("/tasks/:id", h.TaskStatus) // 任务生命周期状态(UI 轮询 submitted/running/done/failed/timeout/waiting/rejected)
|
p.GET("/tasks/:id", h.TaskStatus) // 任务生命周期状态(UI 轮询 submitted/running/done/failed/timeout/waiting/rejected)
|
||||||
p.POST("/tasks/:id/approve", middleware.Audit(db), h.ApproveTask) // HITL 人工审批决定(批准/拒绝,审计)
|
p.POST("/tasks/:id/approve", middleware.Audit(db), h.ApproveTask) // HITL 人工审批决定(批准/拒绝,审计)
|
||||||
p.GET("/tenants/current", h.TenantCurrent) // 当前租户上下文 + 角色(多租户)
|
p.GET("/tenants/current", h.TenantCurrent) // 当前租户上下文 + 角色 + 可花余额(多租户)
|
||||||
|
p.GET("/me/tenants", h.MyTenantsList) // 我所属租户(供切换)
|
||||||
|
p.POST("/me/tenant", h.SwitchTenant) // 切换当前活跃租户
|
||||||
p.GET("/me/usage", h.MyUsage) // 我的用量明细(余额 + 趋势 + 最近消耗)
|
p.GET("/me/usage", h.MyUsage) // 我的用量明细(余额 + 趋势 + 最近消耗)
|
||||||
p.GET("/tasks/:id/eval", h.TaskEval) // 自动化评测结果(综合/质量/忠实度/分级)
|
p.GET("/tasks/:id/eval", h.TaskEval) // 自动化评测结果(综合/质量/忠实度/分级)
|
||||||
p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||||
@@ -105,6 +107,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
|||||||
// 多租户成员管理(平台运维口径)
|
// 多租户成员管理(平台运维口径)
|
||||||
admin.GET("/tenants", h.AdminTenants) // 租户目录(成员数+余额)
|
admin.GET("/tenants", h.AdminTenants) // 租户目录(成员数+余额)
|
||||||
admin.POST("/tenants", h.AdminCreateTenant) // 新建租户(可选指定 owner)
|
admin.POST("/tenants", h.AdminCreateTenant) // 新建租户(可选指定 owner)
|
||||||
|
admin.PUT("/tenants/:id/shared-billing", h.AdminSetSharedBilling) // 共享计费开关
|
||||||
admin.GET("/tenants/:id/members", h.AdminMembers) // 成员列表
|
admin.GET("/tenants/:id/members", h.AdminMembers) // 成员列表
|
||||||
admin.POST("/tenants/:id/members", h.AdminAddMember) // 按邮箱加成员
|
admin.POST("/tenants/:id/members", h.AdminAddMember) // 按邮箱加成员
|
||||||
admin.PUT("/tenants/:id/members/:uid", h.AdminSetMemberRole) // 改角色
|
admin.PUT("/tenants/:id/members/:uid", h.AdminSetMemberRole) // 改角色
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ package store
|
|||||||
// User 是平台用户(Users)。
|
// User 是平台用户(Users)。
|
||||||
type User struct {
|
type User struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Email string `gorm:"uniqueIndex;size:255"`
|
Email string `gorm:"uniqueIndex;size:255"`
|
||||||
Name string `gorm:"size:64"`
|
Name string `gorm:"size:64"`
|
||||||
PasswordHash string `gorm:"size:255" json:"-"` // bcrypt;绝不出 JSON
|
PasswordHash string `gorm:"size:255" json:"-"` // bcrypt;绝不出 JSON
|
||||||
|
ActiveTenantID string `gorm:"size:64" json:"-"` // 当前活跃租户(多租户切换;空=用默认)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task 是一次提交的 Agent 编排任务(DSL)。
|
// Task 是一次提交的 Agent 编排任务(DSL)。
|
||||||
@@ -88,6 +89,7 @@ type Tenant struct {
|
|||||||
Plan string `gorm:"size:32;default:free"` // free / pro / enterprise
|
Plan string `gorm:"size:32;default:free"` // free / pro / enterprise
|
||||||
Status string `gorm:"size:16;default:active"` // active / suspended
|
Status string `gorm:"size:16;default:active"` // active / suspended
|
||||||
CreditBalanceMicro int64 `gorm:"column:credit_balance_micro"` // 物化积分余额 ×10⁻⁶(= credit_ledger 之和;用量扣、充值增)
|
CreditBalanceMicro int64 `gorm:"column:credit_balance_micro"` // 物化积分余额 ×10⁻⁶(= credit_ledger 之和;用量扣、充值增)
|
||||||
|
SharedBilling bool `gorm:"column:shared_billing"` // 共享计费:开=成员消耗计本租户池;关=成员计各自个人池(owner 恒计本租户)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Tenant) TableName() string { return "sundynix_tenant" }
|
func (Tenant) TableName() string { return "sundynix_tenant" }
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ type TenantInfo struct {
|
|||||||
Plan string `json:"plan"`
|
Plan string `json:"plan"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreditBalanceMicro int64 `json:"credit_balance_micro"`
|
CreditBalanceMicro int64 `json:"credit_balance_micro"`
|
||||||
|
SharedBilling bool `json:"shared_billing"`
|
||||||
Members int64 `json:"members"`
|
Members int64 `json:"members"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ func (p *Postgres) ListTenants(ctx context.Context) ([]TenantInfo, error) {
|
|||||||
}
|
}
|
||||||
var out []TenantInfo
|
var out []TenantInfo
|
||||||
err := p.db.WithContext(ctx).Table("sundynix_tenant t").
|
err := p.db.WithContext(ctx).Table("sundynix_tenant t").
|
||||||
Select("t.id, t.name, t.slug, t.plan, t.status, t.credit_balance_micro, "+
|
Select("t.id, t.name, t.slug, t.plan, t.status, t.credit_balance_micro, t.shared_billing, "+
|
||||||
"(SELECT count(*) FROM sundynix_tenant_member m WHERE m.tenant_id = t.id AND m.status = 'active') as members").
|
"(SELECT count(*) FROM sundynix_tenant_member m WHERE m.tenant_id = t.id AND m.status = 'active') as members").
|
||||||
Where("t.deleted_at IS NULL").Order("t.created_at asc").Scan(&out).Error
|
Where("t.deleted_at IS NULL").Order("t.created_at asc").Scan(&out).Error
|
||||||
return out, err
|
return out, err
|
||||||
@@ -287,10 +288,81 @@ func (p *Postgres) MyTenants(ctx context.Context, userID string) ([]TenantInfo,
|
|||||||
}
|
}
|
||||||
var out []TenantInfo
|
var out []TenantInfo
|
||||||
err := p.db.WithContext(ctx).Table("sundynix_tenant t").
|
err := p.db.WithContext(ctx).Table("sundynix_tenant t").
|
||||||
Select("t.id, t.name, t.slug, t.plan, t.status, t.credit_balance_micro, "+
|
Select("t.id, t.name, t.slug, t.plan, t.status, t.credit_balance_micro, t.shared_billing, "+
|
||||||
"(SELECT count(*) FROM sundynix_tenant_member m2 WHERE m2.tenant_id = t.id AND m2.status = 'active') as members").
|
"(SELECT count(*) FROM sundynix_tenant_member m2 WHERE m2.tenant_id = t.id AND m2.status = 'active') as members").
|
||||||
Joins("JOIN sundynix_tenant_member m ON m.tenant_id = t.id").
|
Joins("JOIN sundynix_tenant_member m ON m.tenant_id = t.id").
|
||||||
Where("m.user_id = ? AND m.status = 'active' AND t.deleted_at IS NULL", userID).
|
Where("m.user_id = ? AND m.status = 'active' AND t.deleted_at IS NULL", userID).
|
||||||
Order("t.created_at asc").Scan(&out).Error
|
Order("t.created_at asc").Scan(&out).Error
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 租户切换 + 共享计费(增量2)----
|
||||||
|
|
||||||
|
// PersonalTenantForUser 返回用户自己的个人租户(最早加入且角色 owner 的租户 = 注册默认租户)。
|
||||||
|
func (p *Postgres) PersonalTenantForUser(ctx context.Context, userID string) string {
|
||||||
|
if p.db == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var m TenantMember
|
||||||
|
if err := p.db.WithContext(ctx).Select("tenant_id").
|
||||||
|
Where("user_id = ? AND status = 'active' AND role = ?", userID, RoleOwner).
|
||||||
|
Order("created_at asc").First(&m).Error; err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return m.TenantID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveTenantForUser 解析用户当前活跃租户:user.active_tenant_id 若已设且仍是 active 成员 → 用之;
|
||||||
|
// 否则回退默认租户(并保证默认租户存在)。中间件每请求调用。
|
||||||
|
func (p *Postgres) ActiveTenantForUser(ctx context.Context, userID string) (*Tenant, error) {
|
||||||
|
def, err := p.EnsureDefaultTenant(ctx, userID, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var u User
|
||||||
|
if e := p.db.WithContext(ctx).Select("active_tenant_id").First(&u, "id = ?", userID).Error; e == nil {
|
||||||
|
if u.ActiveTenantID != "" && u.ActiveTenantID != def.ID && p.MemberRole(ctx, u.ActiveTenantID, userID) != "" {
|
||||||
|
if t, _ := p.GetTenant(ctx, u.ActiveTenantID); t != nil {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetActiveTenant 切换用户活跃租户(须为其 active 成员)。
|
||||||
|
func (p *Postgres) SetActiveTenant(ctx context.Context, userID, tenantID string) error {
|
||||||
|
if p.db == nil {
|
||||||
|
return errStoreDisabled
|
||||||
|
}
|
||||||
|
if p.MemberRole(ctx, tenantID, userID) == "" {
|
||||||
|
return errors.New("你不是该租户的成员")
|
||||||
|
}
|
||||||
|
return p.db.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Update("active_tenant_id", tenantID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSharedBilling 设某租户共享计费开关。
|
||||||
|
func (p *Postgres) SetSharedBilling(ctx context.Context, tenantID string, on bool) error {
|
||||||
|
if p.db == nil {
|
||||||
|
return errStoreDisabled
|
||||||
|
}
|
||||||
|
return p.db.WithContext(ctx).Model(&Tenant{}).Where("id = ?", tenantID).Update("shared_billing", on).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveBillingTenantID 决定"这次消耗记谁的池子":本人是 active 租户 owner,或 active 开了
|
||||||
|
// shared_billing → 记 active 租户;否则记本人个人租户(各付各的)。
|
||||||
|
func (p *Postgres) ResolveBillingTenantID(ctx context.Context, userID, activeTenantID string) string {
|
||||||
|
if activeTenantID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if p.MemberRole(ctx, activeTenantID, userID) == RoleOwner {
|
||||||
|
return activeTenantID
|
||||||
|
}
|
||||||
|
if t, _ := p.GetTenant(ctx, activeTenantID); t != nil && t.SharedBilling {
|
||||||
|
return activeTenantID
|
||||||
|
}
|
||||||
|
if personal := p.PersonalTenantForUser(ctx, userID); personal != "" {
|
||||||
|
return personal
|
||||||
|
}
|
||||||
|
return activeTenantID
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user