first commit
This commit is contained in:
112
src/app/bootstrapUserSettings.js
vendored
Normal file
112
src/app/bootstrapUserSettings.js
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes'
|
||||
import Aura from '@primeuix/themes/aura'
|
||||
import Lara from '@primeuix/themes/lara'
|
||||
import Nora from '@primeuix/themes/nora'
|
||||
|
||||
const presets = { Aura, Lara, Nora }
|
||||
|
||||
function safeEq (a, b) {
|
||||
return String(a || '').trim() === String(b || '').trim()
|
||||
}
|
||||
|
||||
// copia do seu getPresetExt (ou exporta ele do Perfil pra reutilizar)
|
||||
function getPresetExt(primaryColors, layoutConfig) {
|
||||
const color = primaryColors.find((c) => c.name === layoutConfig.primary) || { name: 'noir', palette: {} }
|
||||
|
||||
if (color.name === 'noir') {
|
||||
return {
|
||||
semantic: {
|
||||
primary: {
|
||||
50: '{surface.50}', 100: '{surface.100}', 200: '{surface.200}', 300: '{surface.300}',
|
||||
400: '{surface.400}', 500: '{surface.500}', 600: '{surface.600}', 700: '{surface.700}',
|
||||
800: '{surface.800}', 900: '{surface.900}', 950: '{surface.950}'
|
||||
},
|
||||
colorScheme: {
|
||||
light: {
|
||||
primary: { color: '{primary.950}', contrastColor: '#ffffff', hoverColor: '{primary.800}', activeColor: '{primary.700}' },
|
||||
highlight: { background: '{primary.950}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' }
|
||||
},
|
||||
dark: {
|
||||
primary: { color: '{primary.50}', contrastColor: '{primary.950}', hoverColor: '{primary.200}', activeColor: '{primary.300}' },
|
||||
highlight: { background: '{primary.50}', focusBackground: '{primary.300}', color: '{primary.950}', focusColor: '{primary.950}' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
semantic: {
|
||||
primary: color.palette,
|
||||
colorScheme: {
|
||||
light: {
|
||||
primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' },
|
||||
highlight: { background: '{primary.50}', focusBackground: '{primary.100}', color: '{primary.700}', focusColor: '{primary.800}' }
|
||||
},
|
||||
dark: {
|
||||
primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' },
|
||||
highlight: {
|
||||
background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
|
||||
focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
|
||||
color: 'rgba(255,255,255,.87)',
|
||||
focusColor: 'rgba(255,255,255,.87)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function bootstrapUserSettings({
|
||||
primaryColors = [], // passe a lista do seu Perfil (ou uma versão reduzida)
|
||||
surfaces = [] // idem
|
||||
} = {}) {
|
||||
const { layoutConfig, isDarkTheme, toggleDarkMode, changeMenuMode } = useLayout()
|
||||
|
||||
const { data: uRes, error: uErr } = await supabase.auth.getUser()
|
||||
if (uErr) return
|
||||
const user = uRes?.user
|
||||
if (!user) return
|
||||
|
||||
const { data: settings, error } = await supabase
|
||||
.from('user_settings')
|
||||
.select('theme_mode, preset, primary_color, surface_color, menu_mode')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle()
|
||||
|
||||
if (error || !settings) return
|
||||
|
||||
// menu mode
|
||||
if (settings.menu_mode && settings.menu_mode !== layoutConfig.menuMode) {
|
||||
layoutConfig.menuMode = settings.menu_mode
|
||||
changeMenuMode()
|
||||
}
|
||||
|
||||
// preset
|
||||
if (settings.preset && settings.preset !== layoutConfig.preset) {
|
||||
layoutConfig.preset = settings.preset
|
||||
const presetValue = presets[settings.preset] || presets.Aura
|
||||
const surfacePalette = surfaces.find(s => s.name === layoutConfig.surface)?.palette
|
||||
$t().preset(presetValue).preset(getPresetExt(primaryColors, layoutConfig)).surfacePalette(surfacePalette).use({ useDefaultOptions: true })
|
||||
}
|
||||
|
||||
// colors
|
||||
if (settings.primary_color && !safeEq(settings.primary_color, layoutConfig.primary)) {
|
||||
layoutConfig.primary = settings.primary_color
|
||||
updatePreset(getPresetExt(primaryColors, layoutConfig))
|
||||
}
|
||||
|
||||
if (settings.surface_color && !safeEq(settings.surface_color, layoutConfig.surface)) {
|
||||
layoutConfig.surface = settings.surface_color
|
||||
const surface = surfaces.find(s => s.name === settings.surface_color)
|
||||
if (surface) updateSurfacePalette(surface.palette)
|
||||
}
|
||||
|
||||
// dark/light
|
||||
if (settings.theme_mode) {
|
||||
const shouldBeDark = settings.theme_mode === 'dark'
|
||||
if (shouldBeDark !== isDarkTheme) toggleDarkMode()
|
||||
}
|
||||
}
|
||||
222
src/app/session.js
Normal file
222
src/app/session.js
Normal file
@@ -0,0 +1,222 @@
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
/**
|
||||
* ⚠️ IMPORTANTE — ESTABILIDADE DE NAVEGAÇÃO
|
||||
*
|
||||
* Este módulo controla a sessão global usada pelo router guard.
|
||||
*
|
||||
* Já houve uma condição de corrida entre:
|
||||
* - visibilitychange → refreshSession()
|
||||
* - supabase.auth.onAuthStateChange (SIGNED_IN)
|
||||
* - router.beforeEach
|
||||
*
|
||||
* Quando a aba voltava ao foco, refreshSession() era disparado,
|
||||
* o Supabase emitia SIGNED_IN redundante, e o guard aguardava
|
||||
* tenant + entitlements simultaneamente à re-hidratação da sessão.
|
||||
*
|
||||
* Isso fazia a navegação "travar" no meio do beforeEach.
|
||||
*
|
||||
* Para evitar isso:
|
||||
* - initSession usa singleflight (initPromise)
|
||||
* - refreshSession não roda se já estiver refreshing
|
||||
* - SIGNED_IN redundante é ignorado quando estado já está consistente
|
||||
*
|
||||
* NÃO remover esses controles sem entender o fluxo completo
|
||||
* entre sessão, guard e carregamento de tenant/entitlements.
|
||||
*/
|
||||
|
||||
export const sessionUser = ref(null)
|
||||
export const sessionRole = ref(null)
|
||||
export const sessionIsSaasAdmin = ref(false)
|
||||
|
||||
// só no primeiro boot
|
||||
export const sessionReady = ref(false)
|
||||
|
||||
// refresh leve (troca de aba / refresh token) sem desmontar UI
|
||||
export const sessionRefreshing = ref(false)
|
||||
|
||||
let onSignedOutCallback = null
|
||||
export function setOnSignedOut (cb) {
|
||||
onSignedOutCallback = typeof cb === 'function' ? cb : null
|
||||
}
|
||||
|
||||
// evita init concorrente
|
||||
let initPromise = null
|
||||
|
||||
async function fetchRole (userId) {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('role')
|
||||
.eq('id', userId)
|
||||
.single()
|
||||
|
||||
if (error) return null
|
||||
return data?.role || null
|
||||
}
|
||||
|
||||
async function fetchIsSaasAdmin (userId) {
|
||||
const { data, error } = await supabase
|
||||
.from('saas_admins')
|
||||
.select('user_id')
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) return false
|
||||
return !!data
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza estado a partir de uma session "confiável" (getSession() ou callback do auth).
|
||||
* ⚠️ NÃO zera user/role durante refresh enquanto existir sessão.
|
||||
*/
|
||||
async function hydrateFromSession (sess) {
|
||||
const user = sess?.user || null
|
||||
if (!user?.id) return false
|
||||
|
||||
const prevUid = sessionUser.value?.id || null
|
||||
const uid = user.id
|
||||
|
||||
// ✅ pega primeiro hydrate e troca de usuário
|
||||
const userChanged = prevUid !== uid
|
||||
|
||||
// atualiza user imediatamente (sem flicker)
|
||||
sessionUser.value = user
|
||||
|
||||
// ✅ saas admin: calcula no primeiro hydrate e sempre que trocar de user
|
||||
// (no primeiro hydrate prevUid é null, então userChanged = true)
|
||||
if (userChanged) {
|
||||
sessionIsSaasAdmin.value = await fetchIsSaasAdmin(uid)
|
||||
}
|
||||
|
||||
// role: busca se não tem, ou se mudou user
|
||||
if (!sessionRole.value || userChanged) {
|
||||
sessionRole.value = await fetchRole(uid)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot inicial (pode bloquear UI) ou refresh (não pode derrubar menu).
|
||||
*/
|
||||
export async function initSession ({ initial = false } = {}) {
|
||||
if (initPromise) return initPromise
|
||||
|
||||
if (initial) sessionReady.value = false
|
||||
else sessionRefreshing.value = true
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
const { data, error } = await supabase.auth.getSession()
|
||||
if (error) throw error
|
||||
|
||||
const sess = data?.session || null
|
||||
const ok = await hydrateFromSession(sess)
|
||||
|
||||
// se não tem sessão, zera estado (aqui pode, porque é init/refresh controlado)
|
||||
if (!ok) {
|
||||
sessionUser.value = null
|
||||
sessionRole.value = null
|
||||
sessionIsSaasAdmin.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[initSession] getSession falhou (tratando como sem sessão):', e)
|
||||
// não deixa estourar pro router guard
|
||||
sessionUser.value = null
|
||||
sessionRole.value = null
|
||||
sessionIsSaasAdmin.value = false
|
||||
}
|
||||
})()
|
||||
|
||||
try {
|
||||
await initPromise
|
||||
} finally {
|
||||
initPromise = null
|
||||
if (initial) sessionReady.value = true
|
||||
sessionRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// refresh leve (troca de aba etc.)
|
||||
export async function refreshSession () {
|
||||
// ✅ evita corrida: se já está refreshing/init, não dispara outro
|
||||
if (sessionRefreshing.value || initPromise) return
|
||||
|
||||
const { data, error } = await supabase.auth.getSession()
|
||||
if (error) return
|
||||
|
||||
const sess = data?.session || null
|
||||
const uid = sess?.user?.id || null
|
||||
|
||||
// se não tem sessão, não zera aqui (deixa SIGNED_OUT cuidar)
|
||||
if (!uid) return
|
||||
|
||||
// se já está consistente, não faz nada
|
||||
if (sessionUser.value?.id === uid && sessionRole.value) return
|
||||
|
||||
await initSession({ initial: false })
|
||||
}
|
||||
|
||||
// evita múltiplos listeners
|
||||
let authSubscription = null
|
||||
|
||||
export function listenAuthChanges () {
|
||||
if (authSubscription) return
|
||||
|
||||
const { data } = supabase.auth.onAuthStateChange(async (event, sess) => {
|
||||
console.log('[AUTH EVENT]', event)
|
||||
|
||||
// ✅ SIGNED_OUT: zera e chama callback
|
||||
if (event === 'SIGNED_OUT') {
|
||||
sessionUser.value = null
|
||||
sessionRole.value = null
|
||||
sessionIsSaasAdmin.value = false
|
||||
sessionRefreshing.value = false
|
||||
sessionReady.value = true
|
||||
if (onSignedOutCallback) onSignedOutCallback()
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ se já está consistente, ignora SIGNED_IN redundante
|
||||
if (event === 'SIGNED_IN') {
|
||||
const uid = sess?.user?.id || null
|
||||
if (uid && sessionReady.value && sessionUser.value?.id === uid && sessionRole.value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ use a session fornecida no callback
|
||||
if (sess?.user?.id) {
|
||||
// evita reentrância
|
||||
if (sessionRefreshing.value) return
|
||||
|
||||
sessionRefreshing.value = true
|
||||
try {
|
||||
await hydrateFromSession(sess)
|
||||
sessionReady.value = true
|
||||
} catch (e) {
|
||||
console.warn('[auth hydrate error]', e)
|
||||
} finally {
|
||||
sessionRefreshing.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// fallback: refresh leve
|
||||
try {
|
||||
await refreshSession()
|
||||
} catch (e) {
|
||||
console.error('[refreshSession error]', e)
|
||||
}
|
||||
})
|
||||
|
||||
authSubscription = data?.subscription || null
|
||||
}
|
||||
|
||||
export function stopAuthChanges () {
|
||||
if (authSubscription) {
|
||||
authSubscription.unsubscribe()
|
||||
authSubscription = null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user