@innertia-solutions/innertia-nuxt 0.1.46 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.config.ts CHANGED
@@ -149,7 +149,7 @@ export default defineAppConfig({
149
149
  // ─── Tipos públicos ──────────────────────────────────────────────────────────
150
150
  // Re-exportados desde acá para que cualquier composable / componente los importe.
151
151
 
152
- export type InnertiaMode = 'saas' | 'app'
152
+ export type InnertiaMode = 'saas' | 'app' | 'open'
153
153
 
154
154
  export type ContextMobileMode = 'allow' | 'block' | 'redirect'
155
155
 
@@ -30,7 +30,8 @@ const items = computed(() => tab.value === 'unread'
30
30
  ? store.notifications.filter(n => !n.read_at)
31
31
  : store.notifications)
32
32
 
33
- const badge = computed(() => serverUnread.value > 9 ? '9+' : String(serverUnread.value))
33
+ const liveUnread = computed(() => Math.max(serverUnread.value, store.unreadCount))
34
+ const badge = computed(() => liveUnread.value > 9 ? '9+' : String(liveUnread.value))
34
35
 
35
36
  async function load() {
36
37
  loading.value = true
@@ -1,4 +1,4 @@
1
- // useAuthStore, useApi, useOrganizationStore auto-imported
1
+ // useAuthStore, useApi, useOrganizationStore, useInnertiaMode, useGymSelection, useGymStore auto-imported
2
2
 
3
3
  export function useAuth() {
4
4
  const authStore = useAuthStore()
@@ -20,6 +20,15 @@ export function useAuth() {
20
20
  authStore.setCurrentContext(context)
21
21
  queryClient.clear()
22
22
  await fetchMe()
23
+
24
+ // En modo 'open' el tenant se resuelve por selección: cargamos los gyms del
25
+ // usuario y devolvemos un destino (0/1/2+). saas/app conservan su return.
26
+ if (useInnertiaMode().isOpen()) {
27
+ const { resolveAfterLogin } = useGymSelection()
28
+ const gymDestination = await resolveAfterLogin() // 'onboarding' | 'ready' | 'picker'
29
+ return { ...(data ?? {}), gymDestination }
30
+ }
31
+
23
32
  return data
24
33
  }
25
34
 
@@ -68,6 +77,8 @@ export function useAuth() {
68
77
  queryClient.clear()
69
78
  authStore.logout()
70
79
  organizationStore.reset()
80
+ // En open, limpiar también el gym seleccionado
81
+ if (useInnertiaMode().isOpen()) useGymStore().reset()
71
82
  await navigateTo(loginPath)
72
83
  }
73
84
 
@@ -0,0 +1,35 @@
1
+ // useApi, useGymStore auto-imported.
2
+ // Selección de gym en modo 'open': carga los gyms del usuario y resuelve
3
+ // el destino post-login según cuántos tenga (0 / 1 / 2+).
4
+
5
+ export function useGymSelection() {
6
+ const api = useApi()
7
+ const gym = useGymStore()
8
+
9
+ /** Carga los gyms del usuario autenticado y los guarda en el store. */
10
+ async function load() {
11
+ // Shape esperado: { gyms: [{ key, name, status, contexts }] }
12
+ const data = await api.get('auth/my-gyms')
13
+ gym.setGyms(data?.gyms || [])
14
+ return gym.gyms
15
+ }
16
+
17
+ /**
18
+ * Resuelve a dónde llevar al usuario tras el login.
19
+ * 'onboarding' → 0 gyms (debe crear/unirse a uno)
20
+ * 'ready' → 1 gym (auto-seleccionado) o ya tenía uno activo válido
21
+ * 'picker' → 2+ gyms sin selección válida previa
22
+ */
23
+ async function resolveAfterLogin() {
24
+ await load()
25
+ if (gym.gyms.length === 0) return 'onboarding'
26
+ if (gym.gyms.length === 1) { gym.setActive(gym.gyms[0].key); return 'ready' }
27
+ if (gym.activeKey && gym.gyms.some((g) => g.key === gym.activeKey)) return 'ready'
28
+ return 'picker'
29
+ }
30
+
31
+ /** Cambia el gym activo (switcher). */
32
+ function switchTo(key) { gym.setActive(key) }
33
+
34
+ return { load, resolveAfterLogin, switchTo }
35
+ }
@@ -1,7 +1,7 @@
1
1
  import type { InnertiaMode } from '../app.config'
2
2
 
3
3
  /**
4
- * Devuelve el modo activo de la librería: 'saas' | 'app'.
4
+ * Devuelve el modo activo de la librería: 'saas' | 'app' | 'open'.
5
5
  * Default: 'saas' (compatibilidad con productos existentes).
6
6
  *
7
7
  * Se configura en `nuxt.config.ts` del producto:
@@ -10,7 +10,9 @@ import type { InnertiaMode } from '../app.config'
10
10
  * Helpers de conveniencia:
11
11
  * isSaas() → true si mode === 'saas'
12
12
  * isApp() → true si mode === 'app'
13
- * hasTenant() → true si mode === 'saas' (solo saas usa multitenancy)
13
+ * isOpen() → true si mode === 'open'
14
+ * hasTenant() → true si mode === 'saas' | 'open' (usan multitenancy;
15
+ * saas resuelve por subdominio, open por gym seleccionado)
14
16
  */
15
17
  export function useInnertiaMode() {
16
18
  const appConfig = useAppConfig()
@@ -20,6 +22,7 @@ export function useInnertiaMode() {
20
22
  mode,
21
23
  isSaas: () => mode === 'saas',
22
24
  isApp: () => mode === 'app',
23
- hasTenant: () => mode === 'saas',
25
+ isOpen: () => mode === 'open',
26
+ hasTenant: () => mode === 'saas' || mode === 'open',
24
27
  }
25
28
  }
@@ -53,6 +53,29 @@ export function useRealtime() {
53
53
  options.wssPort = pusherWsPort ? Number(pusherWsPort) : 443
54
54
  }
55
55
 
56
+ // Authorizer para canales private-*: postea al /broadcasting/auth del backend
57
+ // con headers frescos (Bearer + X-Tenant) reconstruidos en cada intento.
58
+ const apiBase = config.public.apiBaseUrl || '/api'
59
+ options.channelAuthorization = {
60
+ transport: 'ajax',
61
+ endpoint: `${apiBase}/broadcasting/auth`,
62
+ // Handler propio: reconstruye headers (token/X-Tenant) en cada auth.
63
+ customHandler: async ({ socketId, channelName }, callback) => {
64
+ try {
65
+ const headers = {}
66
+ run(headers)
67
+ const res = await $fetch(`${apiBase}/broadcasting/auth`, {
68
+ method: 'POST',
69
+ headers,
70
+ body: { socket_id: socketId, channel_name: channelName },
71
+ })
72
+ callback(null, res)
73
+ } catch (err) {
74
+ callback(err, null)
75
+ }
76
+ },
77
+ }
78
+
56
79
  pusher.value = new Pusher(pusherAppKey, options)
57
80
 
58
81
  pusher.value.connection.bind('connected', () => {
@@ -2,8 +2,8 @@
2
2
  // Server-only: lee el hostname de la request para extraer el subdomain del tenant.
3
3
  // Solo activo en mode === 'saas'.
4
4
  export default defineNuxtRouteMiddleware((to) => {
5
- // Modo no-saas → no hay tenant que detectar
6
- if (!useInnertiaMode().hasTenant()) return
5
+ // Solo saas resuelve el tenant por subdominio (open lo hace por selección in-app)
6
+ if (!useInnertiaMode().isSaas()) return
7
7
 
8
8
  // Evitar loop infinito: si ya estamos en la página de error de tenant, no redirigir de nuevo.
9
9
  if (to.path === '/tenant-error') return
@@ -3,8 +3,8 @@
3
3
  // Usa la URL interna del backend (apiInternalUrl) para evitar pasar por el proxy de Nitro.
4
4
  // Solo activo en mode === 'saas'.
5
5
  export default defineNuxtRouteMiddleware(async (to) => {
6
- // Modo no-saas → no hay tenant que validar
7
- if (!useInnertiaMode().hasTenant()) return
6
+ // Solo saas valida el tenant por subdominio (open lo hace por selección in-app)
7
+ if (!useInnertiaMode().isSaas()) return
8
8
 
9
9
  if (!import.meta.server) return
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/innertia-nuxt",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "Innertia Solutions — Nuxt layer unificada: core, auth, multitenancy, theme y app contexts",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -1,19 +1,29 @@
1
1
  // Registra el interceptor del header X-Tenant.
2
- // El backend (ResolveTenantFromHeader) identifica el tenant por su key/slug,
3
- // no por UUID. Solo se envía cuando el tenant está validado (tenantId != null)
4
- // para evitar enviar el slug 'local' de dev que no existe en la DB.
2
+ // El backend (ResolveTenantFromHeader) identifica el tenant por su key/slug, no por UUID.
3
+ // - saas: slug del tenant resuelto por subdominio (tenantStore); solo se envía
4
+ // cuando el tenant está validado (tenantId != null) para evitar el 'local' de dev.
5
+ // - open: key del gym seleccionado in-app (gymStore.activeKey).
5
6
  // useRequestInterceptors auto-imported desde nuxt-core.
6
- // useTenantStore auto-imported desde saas stores.
7
- // Solo activo en mode === 'saas'.
7
+ // useTenantStore / useGymStore auto-imported desde stores.
8
+ // Activo en mode === 'saas' | 'open'.
8
9
  export default defineNuxtPlugin(() => {
9
- // Modo no-saas → no inyectar header de tenant
10
- if (!useInnertiaMode().hasTenant()) return
10
+ const { isOpen, hasTenant } = useInnertiaMode()
11
+
12
+ // Modo sin tenant (app) → no inyectar header de tenant
13
+ if (!hasTenant()) return
11
14
 
12
15
  const { add } = useRequestInterceptors()
13
- const tenantStore = useTenantStore()
14
16
 
15
17
  add((headers: Record<string, string>) => {
16
- // Solo enviar si el tenant fue validado (tiene id) y tiene slug
18
+ // open: tenant por gym seleccionado
19
+ if (isOpen()) {
20
+ const gymStore = useGymStore()
21
+ if (gymStore.activeKey) headers['X-Tenant'] = gymStore.activeKey
22
+ return
23
+ }
24
+
25
+ // saas: tenant por subdominio — solo si fue validado (tiene id) y tiene slug
26
+ const tenantStore = useTenantStore()
17
27
  if (tenantStore.tenantId && tenantStore.tenantSlug) {
18
28
  headers['X-Tenant'] = tenantStore.tenantSlug
19
29
  }
package/stores/gym.js ADDED
@@ -0,0 +1,29 @@
1
+ import { defineStore } from 'pinia'
2
+
3
+ /**
4
+ * Gym activo en modo 'open'.
5
+ * A diferencia de saas (tenant por subdominio), open resuelve el tenant por
6
+ * selección in-app: el usuario elige entre sus gyms y esa key viaja como X-Tenant.
7
+ * gyms: [{ key, name, status, contexts }]
8
+ */
9
+ export const useGymStore = defineStore('gym', {
10
+ state: () => ({ activeKey: null, activeContext: null, gyms: [] }),
11
+
12
+ getters: {
13
+ active: (s) => s.gyms.find((g) => g.key === s.activeKey) || null,
14
+ hasMultiple: (s) => s.gyms.length > 1,
15
+ },
16
+
17
+ actions: {
18
+ setGyms(gyms) { this.gyms = gyms || [] },
19
+ setActive(key) { this.activeKey = key },
20
+ // Contexto elegido (backoffice/student/…) — un gym puede otorgar varios.
21
+ setActiveContext(context) { this.activeContext = context },
22
+ reset() { this.activeKey = null; this.activeContext = null; this.gyms = [] },
23
+ },
24
+
25
+ persist: {
26
+ // Key + contexto activos sobreviven el refresh; la lista se recarga del backend.
27
+ pick: ['activeKey', 'activeContext'],
28
+ },
29
+ })