first commit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// src/stores/entitlementsStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
function normalizeKey(k) {
|
||||
return String(k || '').trim()
|
||||
}
|
||||
|
||||
export const useEntitlementsStore = defineStore('entitlements', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
loadedForTenant: null,
|
||||
features: [], // array reativo de feature_key liberadas
|
||||
raw: [],
|
||||
error: null,
|
||||
loadedAt: null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
can: (state) => (featureKey) => state.features.includes(featureKey),
|
||||
has: (state) => (featureKey) => state.features.includes(featureKey)
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetch(tenantId, opts = {}) {
|
||||
return this.loadForTenant(tenantId, opts)
|
||||
},
|
||||
|
||||
clear() {
|
||||
return this.invalidate()
|
||||
},
|
||||
|
||||
/**
|
||||
* Carrega entitlements do tenant.
|
||||
* Importante: quando o plano muda, tenantId é o mesmo,
|
||||
* então você DEVE chamar com { force: true }.
|
||||
*
|
||||
* opts:
|
||||
* - force: ignora cache do tenant
|
||||
* - maxAgeMs: se definido, recarrega quando loadedAt estiver velho
|
||||
*/
|
||||
async loadForTenant(tenantId, { force = false, maxAgeMs = 0 } = {}) {
|
||||
if (!tenantId) {
|
||||
this.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
const sameTenant = this.loadedForTenant === tenantId
|
||||
const hasLoadedAt = typeof this.loadedAt === 'number'
|
||||
const isFresh =
|
||||
sameTenant &&
|
||||
hasLoadedAt &&
|
||||
maxAgeMs > 0 &&
|
||||
Date.now() - this.loadedAt < maxAgeMs
|
||||
|
||||
if (!force && sameTenant && (maxAgeMs === 0 || isFresh)) return
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
// ✅ Modelo B: entitlements por tenant (view)
|
||||
const { data, error } = await supabase
|
||||
.from('v_tenant_entitlements')
|
||||
.select('feature_key')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
const rows = data ?? []
|
||||
|
||||
// normaliza, remove vazios e duplicados
|
||||
const list = []
|
||||
const seen = new Set()
|
||||
for (const r of rows) {
|
||||
const key = normalizeKey(r?.feature_key)
|
||||
if (!key) continue
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
list.push(key)
|
||||
}
|
||||
|
||||
this.raw = rows
|
||||
this.features = list
|
||||
this.loadedForTenant = tenantId
|
||||
this.loadedAt = Date.now()
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.raw = []
|
||||
this.features = []
|
||||
this.loadedForTenant = tenantId
|
||||
this.loadedAt = Date.now()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
invalidate() {
|
||||
this.loadedForTenant = null
|
||||
this.features = []
|
||||
this.raw = []
|
||||
this.error = null
|
||||
this.loadedAt = null
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
// src/stores/saasHealthStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export const useSaasHealthStore = defineStore('saasHealth', {
|
||||
state: () => ({
|
||||
mismatchCount: 0,
|
||||
loading: false,
|
||||
lastLoadedAt: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async loadMismatchCount ({ force = false } = {}) {
|
||||
if (this.loading) return
|
||||
if (!force && this.lastLoadedAt && (Date.now() - this.lastLoadedAt) < 30_000) return // cache 30s
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const { count, error } = await supabase
|
||||
.from('v_subscription_feature_mismatch')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
|
||||
if (error) throw error
|
||||
this.mismatchCount = Number(count || 0)
|
||||
this.lastLoadedAt = Date.now()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
// src/stores/tenantStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export const useTenantStore = defineStore('tenant', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
loaded: false,
|
||||
|
||||
user: null, // auth user
|
||||
memberships: [], // [{ tenant_id, role, status }]
|
||||
activeTenantId: null,
|
||||
activeRole: null,
|
||||
|
||||
needsTenantLink: false,
|
||||
error: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async loadSessionAndTenant () {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
// 1) auth user (estável)
|
||||
const { data, error } = await supabase.auth.getSession()
|
||||
if (error) throw error
|
||||
|
||||
this.user = data?.session?.user ?? null
|
||||
|
||||
// sem sessão -> não chama RPC, só marca estado
|
||||
if (!this.user) {
|
||||
this.memberships = []
|
||||
this.activeTenantId = null
|
||||
this.activeRole = null
|
||||
this.needsTenantLink = false
|
||||
this.loaded = true
|
||||
return
|
||||
}
|
||||
|
||||
// 2) memberships via RPC
|
||||
const { data: mem, error: mErr } = await supabase.rpc('my_tenants')
|
||||
if (mErr) throw mErr
|
||||
|
||||
this.memberships = Array.isArray(mem) ? mem : []
|
||||
|
||||
// 3) define active tenant (primeiro active)
|
||||
const firstActive = this.memberships.find(x => x.status === 'active')
|
||||
this.activeTenantId = firstActive?.tenant_id ?? null
|
||||
this.activeRole = firstActive?.role ?? null
|
||||
|
||||
// se logou mas não tem vínculo ativo
|
||||
this.needsTenantLink = !this.activeTenantId
|
||||
|
||||
this.loaded = true
|
||||
} catch (e) {
|
||||
console.warn('[tenantStore] loadSessionAndTenant falhou:', e)
|
||||
this.error = e
|
||||
|
||||
// ⚠️ NÃO zera tudo agressivamente por erro transitório.
|
||||
// Mantém o que já tinha (se tiver), mas marca loaded pra não travar o app.
|
||||
// Se você preferir ser mais “duro”, só zere quando não houver sessão:
|
||||
// (a sessão já foi lida acima; se der erro antes, user pode estar null)
|
||||
if (!this.user) {
|
||||
this.memberships = []
|
||||
this.activeTenantId = null
|
||||
this.activeRole = null
|
||||
this.needsTenantLink = false
|
||||
}
|
||||
|
||||
this.loaded = true
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
setActiveTenant (tenantId) {
|
||||
const found = this.memberships.find(x => x.tenant_id === tenantId && x.status === 'active')
|
||||
this.activeTenantId = found?.tenant_id ?? null
|
||||
this.activeRole = found?.role ?? null
|
||||
this.needsTenantLink = !this.activeTenantId
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user