@innertia-solutions/innertia-nuxt 0.1.11 → 0.1.12
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/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Auth/LoginForm.vue +1 -1
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Organization/Picker.vue +3 -3
- package/composables/useAuth.js +1 -1
- package/composables/useContext.js +53 -5
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useOrganization.ts +3 -3
- package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/package.json +1 -1
- package/composables/useApp.ts +0 -46
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@innertia-solutions/innertia-nuxt` are documented here.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### BREAKING CHANGES
|
|
8
|
+
|
|
9
|
+
#### apps → contexts rename
|
|
10
|
+
|
|
11
|
+
Aligns frontend with innertia-laravel which already renamed the concept.
|
|
12
|
+
|
|
13
|
+
**Config (`nuxt.config.ts` in your product):**
|
|
14
|
+
```diff
|
|
15
|
+
- innertia.apps: { backoffice: { ... }, technician: { ... } }
|
|
16
|
+
+ innertia.contexts: { backoffice: { ... }, technician: { ... } }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Types:**
|
|
20
|
+
```diff
|
|
21
|
+
- import type { AppDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
22
|
+
+ import type { ContextDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
23
|
+
|
|
24
|
+
- import type { AppMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
25
|
+
+ import type { ContextMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Composables:**
|
|
29
|
+
```diff
|
|
30
|
+
- const { current, all, accessible, canAccess } = useApp()
|
|
31
|
+
+ const { current, all, accessible, canAccess } = useContext()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`useApp()` has been removed. Use `useContext()` for all context/app logic. `useContext()` now also exposes: `contexts` (dict), `currentContext`, `availableContexts`, `switchContext()`, `confirmSwitch()`, `hasAccessToContext()`.
|
|
35
|
+
|
|
36
|
+
**Login body field:**
|
|
37
|
+
|
|
38
|
+
`performLogin()` now sends `context` instead of `app` in the POST body — matching what innertia-laravel now expects.
|
package/app.config.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// appConfig: {
|
|
4
4
|
// innertia: {
|
|
5
|
-
//
|
|
5
|
+
// contexts: { backoffice: {...}, technician: {...} },
|
|
6
6
|
// mobile: { breakpoint: 1024 },
|
|
7
7
|
// },
|
|
8
8
|
// }
|
|
9
9
|
//
|
|
10
|
-
// Si el producto no declara `
|
|
10
|
+
// Si el producto no declara `contexts`, el feature de contextos queda inactivo
|
|
11
11
|
// (middleware no redirige, picker no se muestra).
|
|
12
12
|
export default defineAppConfig({
|
|
13
13
|
innertia: {
|
|
@@ -121,11 +121,11 @@ export default defineAppConfig({
|
|
|
121
121
|
},
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
* Declaración de
|
|
125
|
-
* Cada
|
|
124
|
+
* Declaración de contextos del producto.
|
|
125
|
+
* Cada contexto define un prefijo de URL que mapea a un contexto del backend
|
|
126
126
|
* (matching con `availableContexts` que devuelve `auth/me`).
|
|
127
127
|
*/
|
|
128
|
-
|
|
128
|
+
contexts: {} as Record<string, ContextDefinition>,
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Política mobile global. Se aplica si una `app` no tiene `mobile.mode` propio.
|
|
@@ -144,7 +144,7 @@ export default defineAppConfig({
|
|
|
144
144
|
|
|
145
145
|
export type InnertiaMode = 'saas' | 'app'
|
|
146
146
|
|
|
147
|
-
export type
|
|
147
|
+
export type ContextMobileMode = 'allow' | 'block' | 'redirect'
|
|
148
148
|
|
|
149
149
|
/** Color: nombre de paleta Tailwind o scale custom 50→950. */
|
|
150
150
|
export type ColorScale = Partial<Record<50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950, string>>
|
|
@@ -175,8 +175,8 @@ export interface MenuItem {
|
|
|
175
175
|
pattern?: string
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
export interface
|
|
179
|
-
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del
|
|
178
|
+
export interface ContextDefinition {
|
|
179
|
+
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del contexto. */
|
|
180
180
|
path: string
|
|
181
181
|
/** Clave del contexto que matchea con `availableContexts` del backend. */
|
|
182
182
|
context: string
|
|
@@ -186,13 +186,13 @@ export interface AppDefinition {
|
|
|
186
186
|
description?: string
|
|
187
187
|
/** Nombre del icono de @tabler/icons-vue (sin "Icon"). */
|
|
188
188
|
icon?: string
|
|
189
|
-
/** Ruta de login del
|
|
189
|
+
/** Ruta de login del contexto. */
|
|
190
190
|
loginPath: string
|
|
191
191
|
/** Ruta home (post-login). */
|
|
192
192
|
home: string
|
|
193
193
|
/** Política de uso en mobile. */
|
|
194
194
|
mobile: {
|
|
195
|
-
mode:
|
|
195
|
+
mode: ContextMobileMode
|
|
196
196
|
/** Solo si mode === 'redirect' — adónde mandar desde mobile. */
|
|
197
197
|
redirectTo?: string
|
|
198
198
|
}
|
|
@@ -99,7 +99,7 @@ async function handleOauth(provider: OAuthProvider) {
|
|
|
99
99
|
|
|
100
100
|
// ── Cross-links a otros contextos mobile-friendly (opcional) ───────────────
|
|
101
101
|
const { isMobile } = useMobileGuard()
|
|
102
|
-
const { all } =
|
|
102
|
+
const { all } = useContext()
|
|
103
103
|
const otherMobileApps = computed(() => {
|
|
104
104
|
if (!props.showCrossLinks) return []
|
|
105
105
|
return all.value.filter((a: any) => a.path !== `/${props.context}` && a.mobile?.mode === 'allow')
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { IconDeviceDesktop, IconArrowRight, IconCopy, IconCheck } from '@tabler/icons-vue'
|
|
3
|
-
import type {
|
|
3
|
+
import type { ContextDefinition } from '~/configs/apps'
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{
|
|
6
|
-
/**
|
|
7
|
-
blockedApp?:
|
|
8
|
-
/**
|
|
9
|
-
fallbackApp?:
|
|
6
|
+
/** Context al que se intentó acceder (para el copy contextual). */
|
|
7
|
+
blockedApp?: ContextDefinition | null
|
|
8
|
+
/** Context mobile-friendly al que el usuario puede continuar (opcional). */
|
|
9
|
+
fallbackApp?: ContextDefinition | null
|
|
10
10
|
}>()
|
|
11
11
|
|
|
12
12
|
const route = useRoute()
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import * as TablerIcons from '@tabler/icons-vue'
|
|
3
3
|
import { IconArrowRight } from '@tabler/icons-vue'
|
|
4
|
-
import type {
|
|
4
|
+
import type { ContextDefinition } from '~/configs/apps'
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
|
-
/**
|
|
8
|
-
options:
|
|
7
|
+
/** Contexts a mostrar como opciones de login. */
|
|
8
|
+
options: ContextDefinition[]
|
|
9
9
|
/** Título personalizable. */
|
|
10
10
|
title?: string
|
|
11
11
|
}>()
|
|
12
12
|
|
|
13
13
|
const emit = defineEmits<{
|
|
14
|
-
/** Emitido cuando el usuario elige un
|
|
15
|
-
pick: [
|
|
14
|
+
/** Emitido cuando el usuario elige un context — el padre se encarga de navegar y recordar la elección. */
|
|
15
|
+
pick: [context: ContextDefinition]
|
|
16
16
|
}>()
|
|
17
17
|
|
|
18
18
|
const heading = computed(() => props.title ?? '¿Cómo quieres ingresar?')
|
|
@@ -46,25 +46,25 @@ function resolveIcon(name?: string) {
|
|
|
46
46
|
<!-- Opciones -->
|
|
47
47
|
<div class="space-y-3">
|
|
48
48
|
<button
|
|
49
|
-
v-for="
|
|
50
|
-
:key="
|
|
49
|
+
v-for="context in options"
|
|
50
|
+
:key="context.path"
|
|
51
51
|
type="button"
|
|
52
52
|
class="group w-full flex items-center gap-4 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-4 py-4 text-left hover:border-violet-300 dark:hover:border-violet-700 hover:bg-violet-50/50 dark:hover:bg-violet-900/10 transition-colors"
|
|
53
|
-
@click="emit('pick',
|
|
53
|
+
@click="emit('pick', context)"
|
|
54
54
|
>
|
|
55
55
|
<div class="shrink-0 size-11 rounded-xl bg-violet-100 dark:bg-violet-900/30 flex items-center justify-center group-hover:bg-violet-200 dark:group-hover:bg-violet-900/50 transition-colors">
|
|
56
56
|
<component
|
|
57
|
-
v-if="resolveIcon(
|
|
58
|
-
:is="resolveIcon(
|
|
57
|
+
v-if="resolveIcon(context.icon)"
|
|
58
|
+
:is="resolveIcon(context.icon)"
|
|
59
59
|
class="size-5 text-violet-600 dark:text-violet-400"
|
|
60
60
|
/>
|
|
61
61
|
</div>
|
|
62
62
|
<div class="flex-1 min-w-0">
|
|
63
63
|
<div class="text-base font-semibold text-slate-900 dark:text-white">
|
|
64
|
-
{{
|
|
64
|
+
{{ context.label }}
|
|
65
65
|
</div>
|
|
66
|
-
<div v-if="
|
|
67
|
-
{{
|
|
66
|
+
<div v-if="context.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-2">
|
|
67
|
+
{{ context.description }}
|
|
68
68
|
</div>
|
|
69
69
|
</div>
|
|
70
70
|
<IconArrowRight class="shrink-0 size-5 text-slate-400 group-hover:text-violet-600 dark:group-hover:text-violet-400 transition-colors" />
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { IconBuildingSkyscraper, IconArrowRight } from '@tabler/icons-vue'
|
|
12
12
|
|
|
13
|
-
const { current:
|
|
13
|
+
const { current: currentContext } = useContext()
|
|
14
14
|
const { available, switchTo } = useOrganization()
|
|
15
15
|
|
|
16
16
|
const title = computed(() =>
|
|
17
|
-
|
|
18
|
-
? `Elige una organización para continuar en ${
|
|
17
|
+
currentContext.value
|
|
18
|
+
? `Elige una organización para continuar en ${currentContext.value.label}`
|
|
19
19
|
: 'Elige una organización'
|
|
20
20
|
)
|
|
21
21
|
|
package/composables/useAuth.js
CHANGED
|
@@ -14,7 +14,7 @@ export function useAuth() {
|
|
|
14
14
|
*/
|
|
15
15
|
async function performLogin(context, email, password, remember = false) {
|
|
16
16
|
authStore.rememberUser = remember
|
|
17
|
-
const data = await api.post(`${context}/auth/login`, { email, password,
|
|
17
|
+
const data = await api.post(`${context}/auth/login`, { email, password, context })
|
|
18
18
|
authStore.saveToken(data.token ?? data.access_token)
|
|
19
19
|
authStore.setCurrentContext(context)
|
|
20
20
|
queryClient.clear()
|
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
// useAuthStore, useApi, useAuth auto-imported
|
|
1
|
+
// useAuthStore, useApi, useAuth, useRoute, useAppConfig auto-imported
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import type { ContextDefinition } from '../app.config'
|
|
3
4
|
|
|
4
5
|
export function useContext() {
|
|
6
|
+
const route = useRoute()
|
|
5
7
|
const authStore = useAuthStore()
|
|
8
|
+
const appConfig = useAppConfig()
|
|
6
9
|
const api = useApi()
|
|
7
10
|
const { fetchMe } = useAuth()
|
|
8
11
|
|
|
12
|
+
// ── Config-level: definiciones declaradas en appConfig.innertia.contexts ───
|
|
13
|
+
|
|
14
|
+
/** Diccionario de contextos declarados por el producto. */
|
|
15
|
+
const contexts = computed<Record<string, ContextDefinition>>(() =>
|
|
16
|
+
(appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
/** Todos los contextos declarados, en orden de declaración. */
|
|
20
|
+
const all = computed<ContextDefinition[]>(() => Object.values(contexts.value))
|
|
21
|
+
|
|
22
|
+
/** Contexto actual según URL. `null` si la URL no cae en ningún contexto declarado. */
|
|
23
|
+
const current = computed<ContextDefinition | null>(() => {
|
|
24
|
+
return all.value.find(ctx =>
|
|
25
|
+
route.path === ctx.path || route.path.startsWith(ctx.path + '/')
|
|
26
|
+
) ?? null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/** Contextos a los que el usuario autenticado tiene acceso (filtrado por `availableContexts`). */
|
|
30
|
+
const accessible = computed<ContextDefinition[]>(() => {
|
|
31
|
+
const ctxs = (authStore.availableContexts ?? []) as string[]
|
|
32
|
+
return all.value.filter(ctx => ctxs.includes(ctx.context))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** Helper: ¿el usuario puede acceder a este contexto (por clave de config)? */
|
|
36
|
+
function canAccess(key: string): boolean {
|
|
37
|
+
const ctx = contexts.value[key]
|
|
38
|
+
if (!ctx) return false
|
|
39
|
+
return ((authStore.availableContexts ?? []) as string[]).includes(ctx.context)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Runtime: contexto activo + switching ───────────────────────────────────
|
|
43
|
+
|
|
9
44
|
const currentContext = computed(() => authStore.currentContext)
|
|
10
45
|
const availableContexts = computed(() => authStore.availableContexts)
|
|
11
46
|
|
|
@@ -15,7 +50,7 @@ export function useContext() {
|
|
|
15
50
|
* { success: false, reason: 'no_permission' } — user cannot switch
|
|
16
51
|
* { success: true, requiresConfirmation: true } — show confirmation UI
|
|
17
52
|
*/
|
|
18
|
-
async function switchContext(targetContext) {
|
|
53
|
+
async function switchContext(targetContext: string) {
|
|
19
54
|
const data = await api.get(`auth/context/${targetContext}/check`)
|
|
20
55
|
if (!data.hasAccess) {
|
|
21
56
|
return { success: false, reason: 'no_permission' }
|
|
@@ -27,7 +62,7 @@ export function useContext() {
|
|
|
27
62
|
* Execute the context switch after user confirmation.
|
|
28
63
|
* Updates store and reloads permissions via fetchMe.
|
|
29
64
|
*/
|
|
30
|
-
async function confirmSwitch(targetContext) {
|
|
65
|
+
async function confirmSwitch(targetContext: string) {
|
|
31
66
|
authStore.setCurrentContext(targetContext)
|
|
32
67
|
await fetchMe()
|
|
33
68
|
return { success: true }
|
|
@@ -36,9 +71,22 @@ export function useContext() {
|
|
|
36
71
|
/**
|
|
37
72
|
* Quick synchronous check — is this context in the available list?
|
|
38
73
|
*/
|
|
39
|
-
function hasAccessToContext(context) {
|
|
74
|
+
function hasAccessToContext(context: string) {
|
|
40
75
|
return authStore.availableContexts.includes(context)
|
|
41
76
|
}
|
|
42
77
|
|
|
43
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
// config-level
|
|
80
|
+
contexts,
|
|
81
|
+
all,
|
|
82
|
+
current,
|
|
83
|
+
accessible,
|
|
84
|
+
canAccess,
|
|
85
|
+
// runtime
|
|
86
|
+
currentContext,
|
|
87
|
+
availableContexts,
|
|
88
|
+
switchContext,
|
|
89
|
+
confirmSwitch,
|
|
90
|
+
hasAccessToContext,
|
|
91
|
+
}
|
|
44
92
|
}
|
|
@@ -1,61 +1,61 @@
|
|
|
1
1
|
import { useMediaQuery } from '@vueuse/core'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ContextDefinition } from '../app.config'
|
|
3
3
|
|
|
4
|
-
const PICKER_CHOICE_COOKIE = '
|
|
4
|
+
const PICKER_CHOICE_COOKIE = 'innertia_mobile_context_choice'
|
|
5
5
|
const PICKER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 días
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Detección reactiva de viewport mobile + lógica de landing.
|
|
9
9
|
*
|
|
10
10
|
* Reglas para `landing` (qué mostrar cuando el usuario entra a `/` en mobile):
|
|
11
|
-
* - 0
|
|
12
|
-
* - 1
|
|
13
|
-
* - 2+
|
|
11
|
+
* - 0 contextos mobile-friendly → blocker "abre en escritorio"
|
|
12
|
+
* - 1 contexto mobile-friendly → redirect directo al login de ese contexto
|
|
13
|
+
* - 2+ contextos mobile-friendly → picker (con cookie para recordar última elección)
|
|
14
14
|
*
|
|
15
15
|
* El breakpoint se lee desde `appConfig.innertia.mobile.breakpoint` (default 1024).
|
|
16
16
|
*/
|
|
17
17
|
export function useMobileGuard() {
|
|
18
|
-
const { all, current, accessible } =
|
|
18
|
+
const { all, current, accessible } = useContext()
|
|
19
19
|
const authStore = useAuthStore()
|
|
20
20
|
const appConfig = useAppConfig()
|
|
21
21
|
|
|
22
22
|
const breakpoint: number = appConfig.innertia?.mobile?.breakpoint ?? 1024
|
|
23
23
|
const isMobile = useMediaQuery(`(max-width: ${breakpoint - 1}px)`)
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
const
|
|
25
|
+
/** Contextos con mode 'allow' en mobile. */
|
|
26
|
+
const mobileContexts = computed<ContextDefinition[]>(() =>
|
|
27
27
|
all.value.filter(a => a.mobile?.mode === 'allow')
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
const
|
|
30
|
+
/** Contextos mobile-friendly + accesibles para el usuario autenticado. */
|
|
31
|
+
const mobileAccessibleContexts = computed<ContextDefinition[]>(() =>
|
|
32
32
|
accessible.value.filter(a => a.mobile?.mode === 'allow')
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
/** ¿El
|
|
36
|
-
const
|
|
35
|
+
/** ¿El contexto actual está bloqueado en mobile? */
|
|
36
|
+
const isCurrentContextBlocked = computed<boolean>(() => {
|
|
37
37
|
if (!isMobile.value) return false
|
|
38
38
|
if (!current.value) return false
|
|
39
39
|
return current.value.mobile?.mode === 'block'
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Para el blocker: si el usuario está autenticado y tiene OTRO
|
|
43
|
+
* Para el blocker: si el usuario está autenticado y tiene OTRO contexto mobile-friendly,
|
|
44
44
|
* lo ofrecemos como fallback ("continuar en X").
|
|
45
45
|
*/
|
|
46
|
-
const
|
|
46
|
+
const mobileFallbackContext = computed<ContextDefinition | null>(() => {
|
|
47
47
|
if (!authStore.isAuthenticated()) return null
|
|
48
|
-
return
|
|
48
|
+
return mobileAccessibleContexts.value[0] ?? null
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
// ── Cookie helpers para recordar última elección del picker ─────────────────
|
|
52
|
-
function rememberPickerChoice(
|
|
52
|
+
function rememberPickerChoice(contextKey: string) {
|
|
53
53
|
if (!(appConfig.innertia?.mobile?.rememberChoice ?? true)) return
|
|
54
54
|
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE, {
|
|
55
55
|
maxAge: PICKER_COOKIE_MAX_AGE,
|
|
56
56
|
sameSite: 'lax',
|
|
57
57
|
})
|
|
58
|
-
cookie.value =
|
|
58
|
+
cookie.value = contextKey
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function getRememberedPickerChoice(): string | null {
|
|
@@ -70,10 +70,10 @@ export function useMobileGuard() {
|
|
|
70
70
|
|
|
71
71
|
return {
|
|
72
72
|
isMobile,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
mobileContexts,
|
|
74
|
+
mobileAccessibleContexts,
|
|
75
|
+
isCurrentContextBlocked,
|
|
76
|
+
mobileFallbackContext,
|
|
77
77
|
rememberPickerChoice,
|
|
78
78
|
getRememberedPickerChoice,
|
|
79
79
|
clearRememberedPickerChoice,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composable de organizaciones (sub-tenant scoping).
|
|
3
3
|
*
|
|
4
|
-
* Lee el contexto actual del usuario (via
|
|
4
|
+
* Lee el contexto actual del usuario (via useContext) y expone su org activa + lista
|
|
5
5
|
* de accesibles para ese contexto. Cambiar de contexto rota la org automáticamente.
|
|
6
6
|
*
|
|
7
7
|
* Uso típico:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* <OrganizationSwitcher v-if="available.length > 1" />
|
|
11
11
|
*/
|
|
12
12
|
export function useOrganization() {
|
|
13
|
-
const { current: currentApp } =
|
|
13
|
+
const { current: currentApp } = useContext()
|
|
14
14
|
const organizationStore = useOrganizationStore()
|
|
15
15
|
const appConfig = useAppConfig()
|
|
16
16
|
const { fetchMe } = useAuth()
|
|
@@ -19,7 +19,7 @@ export function useOrganization() {
|
|
|
19
19
|
const allowConsolidated = computed(() => !!appConfig.innertia?.organizations?.allowConsolidated)
|
|
20
20
|
const required = computed(() => !!appConfig.innertia?.organizations?.required)
|
|
21
21
|
|
|
22
|
-
/** Contexto actual (key) — desde
|
|
22
|
+
/** Contexto actual (key) — desde useContext. */
|
|
23
23
|
const currentContext = computed<string | null>(() => currentApp.value?.context ?? null)
|
|
24
24
|
|
|
25
25
|
/** Lista de orgs accesibles para el contexto actual. */
|
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
# innertia-nuxt: apps → contexts Rename — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Rename the "apps" concept to "contexts" throughout innertia-nuxt, aligning the frontend library with the already-renamed innertia-laravel backend.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Pure rename — zero behavior changes. Config key `innertia.apps` → `innertia.contexts`, types `AppDefinition`/`AppMobileMode` → `ContextDefinition`/`ContextMobileMode`, composable `useApp()` merged into `useContext()`, middleware file renamed, login body field corrected. No backward-compat aliases.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Nuxt 4, Vue 3, TypeScript, Pinia. No test suite in this repo — verification is via `grep` for stale references.
|
|
10
|
+
|
|
11
|
+
**Working directory:** `/Users/guillermofarias/Innertia/Dev/workspace/libraries/innertia-nuxt`
|
|
12
|
+
|
|
13
|
+
**Spec:** `docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File Map
|
|
18
|
+
|
|
19
|
+
| File | Action |
|
|
20
|
+
|------|--------|
|
|
21
|
+
| `app.config.ts` | Rename type `AppDefinition` → `ContextDefinition`, `AppMobileMode` → `ContextMobileMode`, config key `innertia.apps` → `innertia.contexts` |
|
|
22
|
+
| `composables/useContext.js` | Absorb all logic from `useApp.ts`; now also reads from `innertia.contexts` and returns `current`, `all`, `accessible`, `canAccess`, `contexts` |
|
|
23
|
+
| `composables/useApp.ts` | **Delete** |
|
|
24
|
+
| `composables/useAuth.js` | Fix login body: `{ app: context }` → `{ context }` |
|
|
25
|
+
| `middleware/03.apps.global.ts` | **Rename** to `03.contexts.global.ts`; update import + internal variable references |
|
|
26
|
+
| `composables/useOrganization.ts` | `useApp()` → `useContext()` |
|
|
27
|
+
| `composables/useMobileGuard.ts` | `useApp()` → `useContext()`, `AppDefinition` → `ContextDefinition`, cookie key renamed |
|
|
28
|
+
| `CHANGELOG.md` | Create — document BREAKING CHANGES |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Task 1: `app.config.ts` — rename types and config key
|
|
33
|
+
|
|
34
|
+
**Files:**
|
|
35
|
+
- Modify: `app.config.ts`
|
|
36
|
+
|
|
37
|
+
- [ ] **Step 1: Replace `innertia.apps` config key with `innertia.contexts`**
|
|
38
|
+
|
|
39
|
+
Find line (currently ~128):
|
|
40
|
+
```ts
|
|
41
|
+
apps: {} as Record<string, AppDefinition>,
|
|
42
|
+
```
|
|
43
|
+
Replace with:
|
|
44
|
+
```ts
|
|
45
|
+
/**
|
|
46
|
+
* Declaración de contextos del producto.
|
|
47
|
+
* Cada contexto define un prefijo de URL que mapea a un contexto del backend
|
|
48
|
+
* (matching con `availableContexts` que devuelve `auth/me`).
|
|
49
|
+
*/
|
|
50
|
+
contexts: {} as Record<string, ContextDefinition>,
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Also update the comment block just above it (currently ~122–127):
|
|
54
|
+
```ts
|
|
55
|
+
/**
|
|
56
|
+
* Declaración de "apps" (contextos) del producto.
|
|
57
|
+
* Cada app define un prefijo de URL que mapea a un contexto del backend
|
|
58
|
+
* (matching con `availableContexts` que devuelve `auth/me`).
|
|
59
|
+
*/
|
|
60
|
+
apps: {} as Record<string, AppDefinition>,
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 2: Rename `AppMobileMode` type**
|
|
64
|
+
|
|
65
|
+
Find line (~147):
|
|
66
|
+
```ts
|
|
67
|
+
export type AppMobileMode = 'allow' | 'block' | 'redirect'
|
|
68
|
+
```
|
|
69
|
+
Replace with:
|
|
70
|
+
```ts
|
|
71
|
+
export type ContextMobileMode = 'allow' | 'block' | 'redirect'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- [ ] **Step 3: Rename `AppDefinition` interface and update its internal reference to `AppMobileMode`**
|
|
75
|
+
|
|
76
|
+
Find (~178–199):
|
|
77
|
+
```ts
|
|
78
|
+
export interface AppDefinition {
|
|
79
|
+
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del app. */
|
|
80
|
+
path: string
|
|
81
|
+
/** Clave del contexto que matchea con `availableContexts` del backend. */
|
|
82
|
+
context: string
|
|
83
|
+
/** Label visible en pickers, dropdowns, etc. */
|
|
84
|
+
label: string
|
|
85
|
+
/** Descripción opcional para el picker mobile. */
|
|
86
|
+
description?: string
|
|
87
|
+
/** Nombre del icono de @tabler/icons-vue (sin "Icon"). */
|
|
88
|
+
icon?: string
|
|
89
|
+
/** Ruta de login del app. */
|
|
90
|
+
loginPath: string
|
|
91
|
+
/** Ruta home (post-login). */
|
|
92
|
+
home: string
|
|
93
|
+
/** Política de uso en mobile. */
|
|
94
|
+
mobile: {
|
|
95
|
+
mode: AppMobileMode
|
|
96
|
+
/** Solo si mode === 'redirect' — adónde mandar desde mobile. */
|
|
97
|
+
redirectTo?: string
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
Replace with:
|
|
102
|
+
```ts
|
|
103
|
+
export interface ContextDefinition {
|
|
104
|
+
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del contexto. */
|
|
105
|
+
path: string
|
|
106
|
+
/** Clave del contexto que matchea con `availableContexts` del backend. */
|
|
107
|
+
context: string
|
|
108
|
+
/** Label visible en pickers, dropdowns, etc. */
|
|
109
|
+
label: string
|
|
110
|
+
/** Descripción opcional para el picker mobile. */
|
|
111
|
+
description?: string
|
|
112
|
+
/** Nombre del icono de @tabler/icons-vue (sin "Icon"). */
|
|
113
|
+
icon?: string
|
|
114
|
+
/** Ruta de login del contexto. */
|
|
115
|
+
loginPath: string
|
|
116
|
+
/** Ruta home (post-login). */
|
|
117
|
+
home: string
|
|
118
|
+
/** Política de uso en mobile. */
|
|
119
|
+
mobile: {
|
|
120
|
+
mode: ContextMobileMode
|
|
121
|
+
/** Solo si mode === 'redirect' — adónde mandar desde mobile. */
|
|
122
|
+
redirectTo?: string
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- [ ] **Step 4: Verify no remaining `AppDefinition` or `AppMobileMode` references in `app.config.ts`**
|
|
128
|
+
|
|
129
|
+
Run:
|
|
130
|
+
```bash
|
|
131
|
+
grep -n "AppDefinition\|AppMobileMode" app.config.ts
|
|
132
|
+
```
|
|
133
|
+
Expected: no output (zero matches).
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 5: Commit**
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git add app.config.ts
|
|
139
|
+
git commit -m "refactor(contexts): rename AppDefinition→ContextDefinition, AppMobileMode→ContextMobileMode, innertia.apps→innertia.contexts in app.config"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### Task 2: `useContext.js` — absorb `useApp` logic
|
|
145
|
+
|
|
146
|
+
**Files:**
|
|
147
|
+
- Modify: `composables/useContext.js`
|
|
148
|
+
|
|
149
|
+
The current `useContext.js` handles runtime context switching. We extend it to also expose the config-level definitions (previously in `useApp.ts`), reading from `appConfig.innertia.contexts`.
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 1: Replace the full contents of `composables/useContext.js`**
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
// useAuthStore, useApi, useAuth auto-imported
|
|
155
|
+
import { computed } from 'vue'
|
|
156
|
+
import type { ContextDefinition } from '../app.config'
|
|
157
|
+
|
|
158
|
+
export function useContext() {
|
|
159
|
+
const route = useRoute()
|
|
160
|
+
const authStore = useAuthStore()
|
|
161
|
+
const appConfig = useAppConfig()
|
|
162
|
+
const api = useApi()
|
|
163
|
+
const { fetchMe } = useAuth()
|
|
164
|
+
|
|
165
|
+
// ── Config-level: definiciones declaradas en appConfig.innertia.contexts ───
|
|
166
|
+
|
|
167
|
+
/** Diccionario de contextos declarados por el producto. */
|
|
168
|
+
const contexts = computed<Record<string, ContextDefinition>>(() =>
|
|
169
|
+
(appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
/** Todos los contextos declarados, en orden de declaración. */
|
|
173
|
+
const all = computed<ContextDefinition[]>(() => Object.values(contexts.value))
|
|
174
|
+
|
|
175
|
+
/** Contexto actual según URL. `null` si la URL no cae en ningún contexto declarado. */
|
|
176
|
+
const current = computed<ContextDefinition | null>(() => {
|
|
177
|
+
return all.value.find(ctx =>
|
|
178
|
+
route.path === ctx.path || route.path.startsWith(ctx.path + '/')
|
|
179
|
+
) ?? null
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
/** Contextos a los que el usuario autenticado tiene acceso (filtrado por `availableContexts`). */
|
|
183
|
+
const accessible = computed<ContextDefinition[]>(() => {
|
|
184
|
+
const ctxs = (authStore.availableContexts ?? []) as string[]
|
|
185
|
+
return all.value.filter(ctx => ctxs.includes(ctx.context))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
/** Helper: ¿el usuario puede acceder a este contexto (por clave de config)? */
|
|
189
|
+
function canAccess(key: string): boolean {
|
|
190
|
+
const ctx = contexts.value[key]
|
|
191
|
+
if (!ctx) return false
|
|
192
|
+
return ((authStore.availableContexts ?? []) as string[]).includes(ctx.context)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Runtime: contexto activo + switching ───────────────────────────────────
|
|
196
|
+
|
|
197
|
+
const currentContext = computed(() => authStore.currentContext)
|
|
198
|
+
const availableContexts = computed(() => authStore.availableContexts)
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check whether user has permission to switch to targetContext.
|
|
202
|
+
* Returns:
|
|
203
|
+
* { success: false, reason: 'no_permission' } — user cannot switch
|
|
204
|
+
* { success: true, requiresConfirmation: true } — show confirmation UI
|
|
205
|
+
*/
|
|
206
|
+
async function switchContext(targetContext) {
|
|
207
|
+
const data = await api.get(`auth/context/${targetContext}/check`)
|
|
208
|
+
if (!data.hasAccess) {
|
|
209
|
+
return { success: false, reason: 'no_permission' }
|
|
210
|
+
}
|
|
211
|
+
return { success: true, requiresConfirmation: true }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Execute the context switch after user confirmation.
|
|
216
|
+
* Updates store and reloads permissions via fetchMe.
|
|
217
|
+
*/
|
|
218
|
+
async function confirmSwitch(targetContext) {
|
|
219
|
+
authStore.setCurrentContext(targetContext)
|
|
220
|
+
await fetchMe()
|
|
221
|
+
return { success: true }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Quick synchronous check — is this context in the available list?
|
|
226
|
+
*/
|
|
227
|
+
function hasAccessToContext(context) {
|
|
228
|
+
return authStore.availableContexts.includes(context)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
// config-level
|
|
233
|
+
contexts,
|
|
234
|
+
all,
|
|
235
|
+
current,
|
|
236
|
+
accessible,
|
|
237
|
+
canAccess,
|
|
238
|
+
// runtime
|
|
239
|
+
currentContext,
|
|
240
|
+
availableContexts,
|
|
241
|
+
switchContext,
|
|
242
|
+
confirmSwitch,
|
|
243
|
+
hasAccessToContext,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- [ ] **Step 2: Verify the file saved correctly**
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
grep -n "innertia.contexts\|ContextDefinition\|contexts\|current\|accessible" composables/useContext.js | head -20
|
|
252
|
+
```
|
|
253
|
+
Expected: lines showing `innertia.contexts`, `ContextDefinition`, and all the returned properties.
|
|
254
|
+
|
|
255
|
+
- [ ] **Step 3: Commit**
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
git add composables/useContext.js
|
|
259
|
+
git commit -m "refactor(contexts): useContext absorbs useApp — exposes current, all, accessible, canAccess, contexts from innertia.contexts config"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### Task 3: Delete `useApp.ts`
|
|
265
|
+
|
|
266
|
+
**Files:**
|
|
267
|
+
- Delete: `composables/useApp.ts`
|
|
268
|
+
|
|
269
|
+
- [ ] **Step 1: Delete the file**
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
git rm composables/useApp.ts
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- [ ] **Step 2: Commit**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
git commit -m "refactor(contexts): remove useApp.ts — functionality merged into useContext"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
### Task 4: `useAuth.js` — fix login body field
|
|
284
|
+
|
|
285
|
+
**Files:**
|
|
286
|
+
- Modify: `composables/useAuth.js`
|
|
287
|
+
|
|
288
|
+
The `performLogin` function currently sends `app: context` in the POST body. The backend (innertia-laravel) now expects the field to be named `context`.
|
|
289
|
+
|
|
290
|
+
- [ ] **Step 1: Fix the `performLogin` call**
|
|
291
|
+
|
|
292
|
+
Find line (~17):
|
|
293
|
+
```js
|
|
294
|
+
const data = await api.post(`${context}/auth/login`, { email, password, app: context })
|
|
295
|
+
```
|
|
296
|
+
Replace with:
|
|
297
|
+
```js
|
|
298
|
+
const data = await api.post(`${context}/auth/login`, { email, password, context })
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 2: Verify**
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
grep -n "app:" composables/useAuth.js
|
|
305
|
+
```
|
|
306
|
+
Expected: no output (zero matches — the old `app: context` field is gone).
|
|
307
|
+
|
|
308
|
+
- [ ] **Step 3: Commit**
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
git add composables/useAuth.js
|
|
312
|
+
git commit -m "fix(contexts): send context field instead of app in performLogin body"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Task 5: Rename and update middleware
|
|
318
|
+
|
|
319
|
+
**Files:**
|
|
320
|
+
- Delete: `middleware/03.apps.global.ts`
|
|
321
|
+
- Create: `middleware/03.contexts.global.ts`
|
|
322
|
+
|
|
323
|
+
- [ ] **Step 1: Create `middleware/03.contexts.global.ts` with updated content**
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
/**
|
|
327
|
+
* Middleware global que se ejecuta en cada navegación.
|
|
328
|
+
*
|
|
329
|
+
* Lee la declaración de contextos desde `appConfig.innertia.contexts`. Si el
|
|
330
|
+
* producto no declara contextos, este middleware no hace nada (feature inactivo).
|
|
331
|
+
*
|
|
332
|
+
* Cuando hay contextos declarados, resuelve qué contexto está intentando acceder
|
|
333
|
+
* el usuario según el prefijo de URL y:
|
|
334
|
+
* 1. Si la URL no cae en ningún contexto declarado → pasa (deja seguir)
|
|
335
|
+
* 2. Si la URL es la ruta de login del contexto → pasa (sin chequeo de permisos)
|
|
336
|
+
* 3. Si el usuario no está autenticado → deja que el middleware `auth` redirija
|
|
337
|
+
* 4. Si `availableContexts` está vacío (SSR sin hidratar) → intenta fetchMe()
|
|
338
|
+
* y si falla, pasa sin redirigir (mejor renderizar que redirigir mal)
|
|
339
|
+
* 5. Si el usuario está autenticado pero NO tiene acceso al contexto →
|
|
340
|
+
* redirige al primer contexto accesible (o a su loginPath si no tiene ninguno)
|
|
341
|
+
* 6. Si todo OK pero `currentContext !== targetContext.context` → sincroniza el
|
|
342
|
+
* contexto en el authStore y recarga permisos vía fetchMe()
|
|
343
|
+
*
|
|
344
|
+
* Esto es lo que permite el "cambio de contexto implícito por URL" sin botón.
|
|
345
|
+
*
|
|
346
|
+
* Numeración 03. para correr DESPUÉS de los middlewares saas
|
|
347
|
+
* (01.detect-subdomain, 02.validate-tenant).
|
|
348
|
+
*/
|
|
349
|
+
import type { ContextDefinition } from '../app.config'
|
|
350
|
+
|
|
351
|
+
export default defineNuxtRouteMiddleware(async (to) => {
|
|
352
|
+
const appConfig = useAppConfig()
|
|
353
|
+
const contexts = (appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
354
|
+
const list = Object.values(contexts)
|
|
355
|
+
|
|
356
|
+
// Si el producto no declaró contextos, el feature está inactivo
|
|
357
|
+
if (list.length === 0) return
|
|
358
|
+
|
|
359
|
+
// ¿La ruta cae bajo algún contexto?
|
|
360
|
+
const targetContext = list.find(ctx =>
|
|
361
|
+
to.path === ctx.path || to.path.startsWith(ctx.path + '/')
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if (!targetContext) return // ruta pública / fuera de contextos
|
|
365
|
+
|
|
366
|
+
// La ruta de login del propio contexto no requiere permisos
|
|
367
|
+
if (to.path === targetContext.loginPath) return
|
|
368
|
+
|
|
369
|
+
const authStore = useAuthStore()
|
|
370
|
+
|
|
371
|
+
// Si no está autenticado, deja que el middleware `auth` de la página haga el redirect
|
|
372
|
+
if (!authStore.isAuthenticated()) return
|
|
373
|
+
|
|
374
|
+
// ── Hidratación defensiva ─────────────────────────────────────────────────
|
|
375
|
+
// `availableContexts` se persiste en localStorage, que NO se lee en SSR.
|
|
376
|
+
// El plugin auth-init hace fetchMe en SSR pero puede fallar silenciosamente
|
|
377
|
+
// (ej. tenant no resuelto). Si llegamos con token válido pero array vacío,
|
|
378
|
+
// intentamos cargar los contextos antes de decidir el redirect.
|
|
379
|
+
if ((authStore.availableContexts ?? []).length === 0) {
|
|
380
|
+
try {
|
|
381
|
+
const { fetchMe } = useAuth()
|
|
382
|
+
await fetchMe()
|
|
383
|
+
} catch {
|
|
384
|
+
// no podemos resolver — pasamos: mejor renderizar la página que redirigir mal
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const userContexts = (authStore.availableContexts ?? []) as string[]
|
|
390
|
+
|
|
391
|
+
// Si tras hidratar seguimos sin contextos, NO redirigimos.
|
|
392
|
+
if (userContexts.length === 0) return
|
|
393
|
+
|
|
394
|
+
// ¿Tiene acceso al contexto?
|
|
395
|
+
if (!userContexts.includes(targetContext.context)) {
|
|
396
|
+
const fallback = list.find(ctx => userContexts.includes(ctx.context))
|
|
397
|
+
if (fallback) {
|
|
398
|
+
return navigateTo(fallback.home, { replace: true })
|
|
399
|
+
}
|
|
400
|
+
return navigateTo(list[0]?.loginPath ?? '/', { replace: true })
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Sincronizar currentContext silenciosamente si cambia
|
|
404
|
+
if (authStore.currentContext !== targetContext.context) {
|
|
405
|
+
authStore.setCurrentContext(targetContext.context)
|
|
406
|
+
try {
|
|
407
|
+
const { fetchMe } = useAuth()
|
|
408
|
+
await fetchMe()
|
|
409
|
+
} catch {
|
|
410
|
+
// best-effort
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- [ ] **Step 2: Remove the old middleware file**
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
git rm middleware/03.apps.global.ts
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- [ ] **Step 3: Stage and commit**
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
git add middleware/03.contexts.global.ts
|
|
426
|
+
git commit -m "refactor(contexts): rename 03.apps.global→03.contexts.global, innertia.apps→innertia.contexts, AppDefinition→ContextDefinition"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### Task 6: `useOrganization.ts` + `useMobileGuard.ts` — swap `useApp()` for `useContext()`
|
|
432
|
+
|
|
433
|
+
**Files:**
|
|
434
|
+
- Modify: `composables/useOrganization.ts`
|
|
435
|
+
- Modify: `composables/useMobileGuard.ts`
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 1: Update `useOrganization.ts` — change `useApp()` to `useContext()`**
|
|
438
|
+
|
|
439
|
+
Find line 13:
|
|
440
|
+
```ts
|
|
441
|
+
const { current: currentApp } = useApp()
|
|
442
|
+
```
|
|
443
|
+
Replace with:
|
|
444
|
+
```ts
|
|
445
|
+
const { current: currentApp } = useContext()
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
(The local alias `currentApp` is kept — it's used on line 23 as `currentApp.value?.context`. No other changes needed in this file.)
|
|
449
|
+
|
|
450
|
+
- [ ] **Step 2: Verify `useOrganization.ts` — no remaining `useApp` references**
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
grep -n "useApp" composables/useOrganization.ts
|
|
454
|
+
```
|
|
455
|
+
Expected: no output.
|
|
456
|
+
|
|
457
|
+
- [ ] **Step 3: Update `useMobileGuard.ts` — full replacement**
|
|
458
|
+
|
|
459
|
+
Replace the full file with:
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
import { useMediaQuery } from '@vueuse/core'
|
|
463
|
+
import type { ContextDefinition } from '../app.config'
|
|
464
|
+
|
|
465
|
+
const PICKER_CHOICE_COOKIE = 'innertia_mobile_context_choice'
|
|
466
|
+
const PICKER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 días
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Detección reactiva de viewport mobile + lógica de landing.
|
|
470
|
+
*
|
|
471
|
+
* Reglas para `landing` (qué mostrar cuando el usuario entra a `/` en mobile):
|
|
472
|
+
* - 0 contextos mobile-friendly → blocker "abre en escritorio"
|
|
473
|
+
* - 1 contexto mobile-friendly → redirect directo al login de ese contexto
|
|
474
|
+
* - 2+ contextos mobile-friendly → picker (con cookie para recordar última elección)
|
|
475
|
+
*
|
|
476
|
+
* El breakpoint se lee desde `appConfig.innertia.mobile.breakpoint` (default 1024).
|
|
477
|
+
*/
|
|
478
|
+
export function useMobileGuard() {
|
|
479
|
+
const { all, current, accessible } = useContext()
|
|
480
|
+
const authStore = useAuthStore()
|
|
481
|
+
const appConfig = useAppConfig()
|
|
482
|
+
|
|
483
|
+
const breakpoint: number = appConfig.innertia?.mobile?.breakpoint ?? 1024
|
|
484
|
+
const isMobile = useMediaQuery(`(max-width: ${breakpoint - 1}px)`)
|
|
485
|
+
|
|
486
|
+
/** Contextos con mode 'allow' en mobile. */
|
|
487
|
+
const mobileApps = computed<ContextDefinition[]>(() =>
|
|
488
|
+
all.value.filter(a => a.mobile?.mode === 'allow')
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
/** Contextos mobile-friendly + accesibles para el usuario autenticado. */
|
|
492
|
+
const mobileAccessibleApps = computed<ContextDefinition[]>(() =>
|
|
493
|
+
accessible.value.filter(a => a.mobile?.mode === 'allow')
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
/** ¿El contexto actual está bloqueado en mobile? */
|
|
497
|
+
const isCurrentAppBlocked = computed<boolean>(() => {
|
|
498
|
+
if (!isMobile.value) return false
|
|
499
|
+
if (!current.value) return false
|
|
500
|
+
return current.value.mobile?.mode === 'block'
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Para el blocker: si el usuario está autenticado y tiene OTRO contexto mobile-friendly,
|
|
505
|
+
* lo ofrecemos como fallback ("continuar en X").
|
|
506
|
+
*/
|
|
507
|
+
const mobileFallbackApp = computed<ContextDefinition | null>(() => {
|
|
508
|
+
if (!authStore.isAuthenticated()) return null
|
|
509
|
+
return mobileAccessibleApps.value[0] ?? null
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
// ── Cookie helpers para recordar última elección del picker ─────────────────
|
|
513
|
+
function rememberPickerChoice(contextKey: string) {
|
|
514
|
+
if (!(appConfig.innertia?.mobile?.rememberChoice ?? true)) return
|
|
515
|
+
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE, {
|
|
516
|
+
maxAge: PICKER_COOKIE_MAX_AGE,
|
|
517
|
+
sameSite: 'lax',
|
|
518
|
+
})
|
|
519
|
+
cookie.value = contextKey
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function getRememberedPickerChoice(): string | null {
|
|
523
|
+
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE)
|
|
524
|
+
return cookie.value ?? null
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function clearRememberedPickerChoice() {
|
|
528
|
+
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE)
|
|
529
|
+
cookie.value = null
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
isMobile,
|
|
534
|
+
mobileApps,
|
|
535
|
+
mobileAccessibleApps,
|
|
536
|
+
isCurrentAppBlocked,
|
|
537
|
+
mobileFallbackApp,
|
|
538
|
+
rememberPickerChoice,
|
|
539
|
+
getRememberedPickerChoice,
|
|
540
|
+
clearRememberedPickerChoice,
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
- [ ] **Step 4: Verify no remaining `useApp` or `AppDefinition` references in both files**
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
grep -n "useApp\|AppDefinition\|AppMobileMode" composables/useOrganization.ts composables/useMobileGuard.ts
|
|
549
|
+
```
|
|
550
|
+
Expected: no output.
|
|
551
|
+
|
|
552
|
+
- [ ] **Step 5: Commit**
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
git add composables/useOrganization.ts composables/useMobileGuard.ts
|
|
556
|
+
git commit -m "refactor(contexts): useOrganization + useMobileGuard — useApp()→useContext(), AppDefinition→ContextDefinition"
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
### Task 7: Docs — CHANGELOG + final verification
|
|
562
|
+
|
|
563
|
+
**Files:**
|
|
564
|
+
- Create: `CHANGELOG.md`
|
|
565
|
+
|
|
566
|
+
- [ ] **Step 1: Create `CHANGELOG.md`**
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
cat > CHANGELOG.md << 'EOF'
|
|
570
|
+
# Changelog
|
|
571
|
+
|
|
572
|
+
All notable changes to `@innertia-solutions/innertia-nuxt` are documented here.
|
|
573
|
+
|
|
574
|
+
## [Unreleased]
|
|
575
|
+
|
|
576
|
+
### BREAKING CHANGES
|
|
577
|
+
|
|
578
|
+
#### apps → contexts rename
|
|
579
|
+
|
|
580
|
+
Aligns frontend with innertia-laravel which already renamed the concept.
|
|
581
|
+
|
|
582
|
+
**Config (`app.config.ts` / `nuxt.config.ts` in your product):**
|
|
583
|
+
```diff
|
|
584
|
+
- innertia.apps: { backoffice: { ... }, technician: { ... } }
|
|
585
|
+
+ innertia.contexts: { backoffice: { ... }, technician: { ... } }
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Types:**
|
|
589
|
+
```diff
|
|
590
|
+
- import type { AppDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
591
|
+
+ import type { ContextDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
592
|
+
|
|
593
|
+
- import type { AppMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
594
|
+
+ import type { ContextMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**Composables:**
|
|
598
|
+
```diff
|
|
599
|
+
- const { current, all, accessible, canAccess } = useApp()
|
|
600
|
+
+ const { current, all, accessible, canAccess } = useContext()
|
|
601
|
+
# useContext() now also returns: contexts (dict), currentContext, availableContexts,
|
|
602
|
+
# switchContext(), confirmSwitch(), hasAccessToContext()
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
`useApp()` has been removed. Use `useContext()` for all context/app logic.
|
|
606
|
+
|
|
607
|
+
**Login body field:**
|
|
608
|
+
|
|
609
|
+
The `performLogin()` function in `useAuth` now sends `context` instead of `app` in
|
|
610
|
+
the POST body — matching what innertia-laravel now expects.
|
|
611
|
+
EOF
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
- [ ] **Step 2: Final grep — verify no stale references remain in the entire library**
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
grep -r "innertia\.apps\|AppDefinition\|AppMobileMode\|useApp()\|'app:'\|\"app:\"" \
|
|
618
|
+
--include="*.ts" --include="*.js" --include="*.vue" \
|
|
619
|
+
--exclude-dir=".nuxt" --exclude-dir="node_modules" \
|
|
620
|
+
.
|
|
621
|
+
```
|
|
622
|
+
Expected: **no output**. If any matches appear, fix them before committing.
|
|
623
|
+
|
|
624
|
+
- [ ] **Step 3: Commit**
|
|
625
|
+
|
|
626
|
+
```bash
|
|
627
|
+
git add CHANGELOG.md
|
|
628
|
+
git commit -m "docs(contexts): add CHANGELOG with apps→contexts BREAKING CHANGE"
|
|
629
|
+
```
|
|
630
|
+
EOF
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# innertia-nuxt: Rename apps → contexts
|
|
2
|
+
|
|
3
|
+
**Fecha:** 2026-05-27
|
|
4
|
+
**Estado:** Aprobado para implementación
|
|
5
|
+
**Tipo:** Refactor — rename puro, sin cambios de comportamiento
|
|
6
|
+
**Scope:** innertia-nuxt library
|
|
7
|
+
|
|
8
|
+
## Problema
|
|
9
|
+
|
|
10
|
+
El concepto de "apps" (áreas del sistema a las que un usuario tiene acceso) colisiona semánticamente con "apps" en el contexto de frontend (aplicaciones, secciones de front). Renombrar a "contexts" elimina la ambigüedad y alinea la librería con el rename ya realizado en innertia-laravel (donde `user_apps`, `HasApps`, `appKeys()` etc. ya se renombraron a `user_contexts`, `HasContexts`, `contextKeys()`).
|
|
11
|
+
|
|
12
|
+
El backend ya devuelve `availableContexts` y `currentContext` — el frontend ya usaba esos nombres en el store. Este rename hace que la **capa de configuración y composables** sea consistente con la API que ya consume.
|
|
13
|
+
|
|
14
|
+
## Alcance
|
|
15
|
+
|
|
16
|
+
Rename puro. **Cero cambios de comportamiento.** Todo lo que hoy funciona, sigue funcionando igual — solo cambian los nombres.
|
|
17
|
+
|
|
18
|
+
**Fuera de scope:**
|
|
19
|
+
- `menuApps` en `app.config.ts` y `backoffice.vue` — semánticamente correcto como "secciones del menú", no relacionado con el sistema de user contexts. Se mantiene intacto.
|
|
20
|
+
- Lógica de organizations, mobile guard, o cualquier otra feature.
|
|
21
|
+
|
|
22
|
+
## Mapa de renombrado
|
|
23
|
+
|
|
24
|
+
### `app.config.ts` — config key + tipos
|
|
25
|
+
|
|
26
|
+
| Antes | Después |
|
|
27
|
+
|-------|---------|
|
|
28
|
+
| `innertia.apps: {}` | `innertia.contexts: {}` |
|
|
29
|
+
| `AppDefinition` interface | `ContextDefinition` |
|
|
30
|
+
| `AppMobileMode` type | `ContextMobileMode` |
|
|
31
|
+
|
|
32
|
+
`menuApps` **no se toca**.
|
|
33
|
+
|
|
34
|
+
### `composables/useContext.js` — fusión de useApp + useContext
|
|
35
|
+
|
|
36
|
+
`useApp.ts` se elimina. `useContext.js` absorbe su lógica y expone la unión completa:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const {
|
|
40
|
+
// — absorbido de useApp —
|
|
41
|
+
current, // ContextDefinition | null (contexto según URL actual)
|
|
42
|
+
all, // ContextDefinition[] (todos los declarados en config)
|
|
43
|
+
accessible, // ContextDefinition[] (filtrados por availableContexts del user)
|
|
44
|
+
canAccess, // (key: string) => boolean
|
|
45
|
+
contexts, // Record<string, ContextDefinition> (dict raw del config)
|
|
46
|
+
|
|
47
|
+
// — ya existía en useContext —
|
|
48
|
+
currentContext, // string | null
|
|
49
|
+
availableContexts, // string[]
|
|
50
|
+
switchContext, // (target: string) => Promise<{success, reason?}>
|
|
51
|
+
confirmSwitch, // (target: string) => Promise<{success}>
|
|
52
|
+
hasAccessToContext, // (context: string) => boolean
|
|
53
|
+
} = useContext()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Regla de resolución de la parte absorbida:** lee de `appConfig.innertia.contexts` (antes `appConfig.innertia.apps`). La lógica interna es idéntica a la de `useApp.ts` actual.
|
|
57
|
+
|
|
58
|
+
### `composables/useApp.ts`
|
|
59
|
+
|
|
60
|
+
**Eliminado.** Toda su funcionalidad vive en `useContext.js`.
|
|
61
|
+
|
|
62
|
+
### `composables/useAuth.js`
|
|
63
|
+
|
|
64
|
+
Login POST body:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
// Antes
|
|
68
|
+
api.post(`${context}/auth/login`, { email, password, app: context })
|
|
69
|
+
|
|
70
|
+
// Después
|
|
71
|
+
api.post(`${context}/auth/login`, { email, password, context })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
El backend (innertia-laravel) ya espera el campo `context`. Este fix cierra la inconsistencia.
|
|
75
|
+
|
|
76
|
+
### `middleware/03.apps.global.ts` → `middleware/03.contexts.global.ts`
|
|
77
|
+
|
|
78
|
+
- Renombrar el archivo.
|
|
79
|
+
- Internamente: `apps` variables → `contexts`, `AppDefinition` import → `ContextDefinition`, `appConfig.innertia.apps` → `appConfig.innertia.contexts`.
|
|
80
|
+
- La lógica de los 6 escenarios (ruta pública, login path, sin auth, sin contextos, sin acceso, sync) no cambia.
|
|
81
|
+
|
|
82
|
+
### `composables/useOrganization.ts`
|
|
83
|
+
|
|
84
|
+
- `useApp()` → `useContext()`
|
|
85
|
+
- Las propiedades que consume (`current`, referenciada internamente como `currentApp`) siguen existiendo con el mismo nombre en el composable fusionado.
|
|
86
|
+
|
|
87
|
+
### `composables/useMobileGuard.ts`
|
|
88
|
+
|
|
89
|
+
- `useApp()` → `useContext()`
|
|
90
|
+
- Propiedades consumidas: `all`, `current`, `accessible` — todas disponibles en el composable fusionado sin cambios de nombre.
|
|
91
|
+
|
|
92
|
+
### `CHANGELOG.md`
|
|
93
|
+
|
|
94
|
+
Agregar entrada `[Unreleased]` con sección `### BREAKING CHANGES`.
|
|
95
|
+
|
|
96
|
+
## Archivos a modificar (8)
|
|
97
|
+
|
|
98
|
+
1. `app.config.ts` — config key + tipos
|
|
99
|
+
2. `composables/useContext.js` — absorber useApp + actualizar internals
|
|
100
|
+
3. `composables/useApp.ts` — **eliminar**
|
|
101
|
+
4. `composables/useAuth.js` — fix body del login
|
|
102
|
+
5. `middleware/03.apps.global.ts` → **renombrar** a `03.contexts.global.ts` + editar internals
|
|
103
|
+
6. `composables/useOrganization.ts` — `useApp()` → `useContext()`
|
|
104
|
+
7. `composables/useMobileGuard.ts` — `useApp()` → `useContext()`
|
|
105
|
+
8. `CHANGELOG.md` — BREAKING CHANGE entry
|
|
106
|
+
|
|
107
|
+
## BREAKING CHANGES para productos consumidores
|
|
108
|
+
|
|
109
|
+
- Config `innertia.contexts` en vez de `innertia.apps`
|
|
110
|
+
- Tipo `ContextDefinition` en vez de `AppDefinition`
|
|
111
|
+
- Tipo `ContextMobileMode` en vez de `AppMobileMode`
|
|
112
|
+
- `useApp()` eliminado → usar `useContext()` (que ahora expone también `current`, `all`, `accessible`, `canAccess`, `contexts`)
|
|
113
|
+
- `performLogin` body: campo `context` en vez de `app`
|
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Middleware global que se ejecuta en cada navegación.
|
|
3
3
|
*
|
|
4
|
-
* Lee la declaración de
|
|
5
|
-
* no declara
|
|
4
|
+
* Lee la declaración de contextos desde `appConfig.innertia.contexts`. Si el
|
|
5
|
+
* producto no declara contextos, este middleware no hace nada (feature inactivo).
|
|
6
6
|
*
|
|
7
|
-
* Cuando hay
|
|
8
|
-
*
|
|
9
|
-
* 1. Si la URL no cae en ningún
|
|
10
|
-
* 2. Si la URL es la ruta de login del
|
|
7
|
+
* Cuando hay contextos declarados, resuelve qué contexto está intentando acceder
|
|
8
|
+
* el usuario según el prefijo de URL y:
|
|
9
|
+
* 1. Si la URL no cae en ningún contexto declarado → pasa (deja seguir)
|
|
10
|
+
* 2. Si la URL es la ruta de login del contexto → pasa (sin chequeo de permisos)
|
|
11
11
|
* 3. Si el usuario no está autenticado → deja que el middleware `auth` redirija
|
|
12
12
|
* 4. Si `availableContexts` está vacío (SSR sin hidratar) → intenta fetchMe()
|
|
13
13
|
* y si falla, pasa sin redirigir (mejor renderizar que redirigir mal)
|
|
14
|
-
* 5. Si el usuario está autenticado pero NO tiene acceso al contexto
|
|
15
|
-
* redirige al primer
|
|
16
|
-
* 6. Si todo OK pero `currentContext !==
|
|
17
|
-
* en el authStore y recarga permisos vía fetchMe()
|
|
14
|
+
* 5. Si el usuario está autenticado pero NO tiene acceso al contexto →
|
|
15
|
+
* redirige al primer contexto accesible (o a su loginPath si no tiene ninguno)
|
|
16
|
+
* 6. Si todo OK pero `currentContext !== targetContext.context` → sincroniza el
|
|
17
|
+
* contexto en el authStore y recarga permisos vía fetchMe()
|
|
18
18
|
*
|
|
19
19
|
* Esto es lo que permite el "cambio de contexto implícito por URL" sin botón.
|
|
20
20
|
*
|
|
21
21
|
* Numeración 03. para correr DESPUÉS de los middlewares saas
|
|
22
22
|
* (01.detect-subdomain, 02.validate-tenant).
|
|
23
23
|
*/
|
|
24
|
-
import type {
|
|
24
|
+
import type { ContextDefinition } from '../app.config'
|
|
25
25
|
|
|
26
26
|
export default defineNuxtRouteMiddleware(async (to) => {
|
|
27
27
|
const appConfig = useAppConfig()
|
|
28
|
-
const
|
|
29
|
-
const list = Object.values(
|
|
28
|
+
const contexts = (appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
29
|
+
const list = Object.values(contexts)
|
|
30
30
|
|
|
31
|
-
// Si el producto no declaró
|
|
31
|
+
// Si el producto no declaró contextos, el feature está inactivo
|
|
32
32
|
if (list.length === 0) return
|
|
33
33
|
|
|
34
|
-
// ¿La ruta cae bajo algún
|
|
35
|
-
const
|
|
36
|
-
to.path ===
|
|
34
|
+
// ¿La ruta cae bajo algún contexto?
|
|
35
|
+
const targetContext = list.find(ctx =>
|
|
36
|
+
to.path === ctx.path || to.path.startsWith(ctx.path + '/')
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
if (!
|
|
39
|
+
if (!targetContext) return // ruta pública / fuera de contextos
|
|
40
40
|
|
|
41
|
-
// La ruta de login del propio
|
|
42
|
-
if (to.path ===
|
|
41
|
+
// La ruta de login del propio contexto no requiere permisos
|
|
42
|
+
if (to.path === targetContext.loginPath) return
|
|
43
43
|
|
|
44
44
|
const authStore = useAuthStore()
|
|
45
45
|
|
|
@@ -66,9 +66,9 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
66
66
|
// Si tras hidratar seguimos sin contextos, NO redirigimos.
|
|
67
67
|
if (userContexts.length === 0) return
|
|
68
68
|
|
|
69
|
-
// ¿Tiene acceso al contexto
|
|
70
|
-
if (!userContexts.includes(
|
|
71
|
-
const fallback = list.find(
|
|
69
|
+
// ¿Tiene acceso al contexto?
|
|
70
|
+
if (!userContexts.includes(targetContext.context)) {
|
|
71
|
+
const fallback = list.find(ctx => userContexts.includes(ctx.context))
|
|
72
72
|
if (fallback) {
|
|
73
73
|
return navigateTo(fallback.home, { replace: true })
|
|
74
74
|
}
|
|
@@ -76,8 +76,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Sincronizar currentContext silenciosamente si cambia
|
|
79
|
-
if (authStore.currentContext !==
|
|
80
|
-
authStore.setCurrentContext(
|
|
79
|
+
if (authStore.currentContext !== targetContext.context) {
|
|
80
|
+
authStore.setCurrentContext(targetContext.context)
|
|
81
81
|
try {
|
|
82
82
|
const { fetchMe } = useAuth()
|
|
83
83
|
await fetchMe()
|
package/package.json
CHANGED
package/composables/useApp.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { AppDefinition } from '../app.config'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Devuelve metadata del app actual + listas filtradas por permisos del usuario.
|
|
5
|
-
* Lee la declaración de apps desde `appConfig.innertia.apps` (configurable por
|
|
6
|
-
* el producto en su `nuxt.config.ts`).
|
|
7
|
-
*
|
|
8
|
-
* Uso típico:
|
|
9
|
-
* const { current, accessible } = useApp()
|
|
10
|
-
* <h1>Estás en: {{ current?.label }}</h1>
|
|
11
|
-
*/
|
|
12
|
-
export function useApp() {
|
|
13
|
-
const route = useRoute()
|
|
14
|
-
const authStore = useAuthStore()
|
|
15
|
-
const appConfig = useAppConfig()
|
|
16
|
-
|
|
17
|
-
/** Diccionario de apps declarados por el producto. */
|
|
18
|
-
const apps = computed<Record<string, AppDefinition>>(() =>
|
|
19
|
-
(appConfig.innertia?.apps ?? {}) as Record<string, AppDefinition>
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
/** Todos los apps declarados, en orden de declaración. */
|
|
23
|
-
const all = computed<AppDefinition[]>(() => Object.values(apps.value))
|
|
24
|
-
|
|
25
|
-
/** App actual, determinado por el prefijo de la ruta. `null` si la URL no cae en ningún app. */
|
|
26
|
-
const current = computed<AppDefinition | null>(() => {
|
|
27
|
-
return all.value.find(app =>
|
|
28
|
-
route.path === app.path || route.path.startsWith(app.path + '/')
|
|
29
|
-
) ?? null
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
/** Apps a los que el usuario autenticado tiene acceso (filtrado por `availableContexts`). */
|
|
33
|
-
const accessible = computed<AppDefinition[]>(() => {
|
|
34
|
-
const ctxs = (authStore.availableContexts ?? []) as string[]
|
|
35
|
-
return all.value.filter(app => ctxs.includes(app.context))
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
/** Helper: ¿el usuario puede acceder a este app? */
|
|
39
|
-
function canAccess(appKey: string): boolean {
|
|
40
|
-
const app = apps.value[appKey]
|
|
41
|
-
if (!app) return false
|
|
42
|
-
return ((authStore.availableContexts ?? []) as string[]).includes(app.context)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { current, all, accessible, canAccess, apps }
|
|
46
|
-
}
|