@innertia-solutions/innertia-nuxt 0.1.59 → 0.1.60

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.
@@ -42,6 +42,14 @@ export function useApi() {
42
42
  }
43
43
  run(headers, options)
44
44
 
45
+ // SSR: adjuntar el Bearer desde la sesión sellada del request activo
46
+ // (el BFF no interviene en llamadas server-side directas a Laravel).
47
+ if (import.meta.server) {
48
+ const event = useRequestEvent()
49
+ const token = event?.context?.authToken
50
+ if (token) headers['Authorization'] = `Bearer ${token}`
51
+ }
52
+
45
53
  const cleanPath = path.startsWith('/') ? path.slice(1) : path
46
54
  let url = `${baseUrl}/${cleanPath}`
47
55
 
@@ -14,9 +14,13 @@ export function useAuth() {
14
14
  */
15
15
  async function performLogin(context, email, password, remember = false) {
16
16
  authStore.rememberUser = remember
17
- // Endpoint agnóstico de contexto: el contexto viaja en el payload (y luego en el JWT).
18
- const data = await api.post('auth/login', { email, password, context })
19
- authStore.saveToken(data.token ?? data.access_token)
17
+ // Sella la sesión en el server (Nitro). Devuelve user+contexts, SIN token.
18
+ const data = await $fetch('/_auth/login', {
19
+ method: 'POST',
20
+ body: { context, email, password, remember },
21
+ })
22
+ authStore.setAuthenticated(true)
23
+ if (data.user) authStore.saveUser(data.user)
20
24
  authStore.setCurrentContext(context)
21
25
  queryClient.clear()
22
26
  await fetchMe()
@@ -69,13 +73,9 @@ export function useAuth() {
69
73
  * Logout: best-effort POST to backend, then clear local state and redirect.
70
74
  */
71
75
  async function logout() {
72
- try {
73
- await api.post('auth/logout', {})
74
- } catch {
75
- // best-effort — ignore network failures
76
- }
76
+ try { await $fetch('/_auth/logout', { method: 'POST' }) } catch { /* best-effort */ }
77
77
  queryClient.clear()
78
- authStore.logout()
78
+ authStore.logout() // limpia el state en memoria
79
79
  organizationStore.reset()
80
80
  // En open, limpiar también el tenant seleccionado
81
81
  if (useInnertiaMode().isOpen()) useActiveTenantStore().reset()
@@ -87,10 +87,11 @@ export function useAuth() {
87
87
  * Returns the URL string from the backend.
88
88
  */
89
89
  async function getOauthRedirectUrl(context, provider) {
90
- // Endpoint agnóstico: el contexto viaja como query param (el backend lo
91
- // codifica en el state de OAuth y lo recupera en el callback).
92
- const data = await api.get(`auth/${provider}/redirect`, { params: { context } })
93
- return data.url
90
+ // Laravel arma la URL de Google, pero el redirect_uri debe volver a la ruta
91
+ // Nitro que sella la sesión: /_auth/{provider}/callback
92
+ const redirectUri = `${window.location.origin}/_auth/${provider}/callback?context=${context}`
93
+ const data = await api.get(`auth/${provider}/redirect`, { params: { context, redirect_uri: redirectUri } })
94
+ return data.url ?? data
94
95
  }
95
96
 
96
97
  return { performLogin, fetchMe, logout, getOauthRedirectUrl }
@@ -63,7 +63,12 @@ export function useContext() {
63
63
  * Updates store and reloads permissions via fetchMe.
64
64
  */
65
65
  async function confirmSwitch(targetContext: string) {
66
- authStore.setCurrentContext(targetContext)
66
+ const context = targetContext
67
+ authStore.setCurrentContext(context)
68
+ // Sincronizar el contexto activo en la sesión sellada (para SSR).
69
+ if (import.meta.client) {
70
+ try { await $fetch('/_auth/context', { method: 'PUT', body: { context } }) } catch { /* best-effort */ }
71
+ }
67
72
  await fetchMe()
68
73
  return { success: true }
69
74
  }
package/nuxt.config.ts CHANGED
@@ -22,6 +22,10 @@ export default defineNuxtConfig({
22
22
  ],
23
23
  },
24
24
  runtimeConfig: {
25
+ // Privado (solo server). Llenado desde NUXT_SESSION_PASSWORD (≥32 chars).
26
+ session: {
27
+ password: '',
28
+ },
25
29
  public: {
26
30
  loginPath: '/backoffice/login',
27
31
  homePath: '/backoffice',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/innertia-nuxt",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "Innertia Solutions — Nuxt layer unificada: core, auth, multitenancy, theme y app contexts",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -56,11 +56,15 @@
56
56
  "vanilla-calendar-pro": "^3.1.0",
57
57
  "vue3-apexcharts": "^1.11.1"
58
58
  },
59
+ "scripts": {
60
+ "test": "vitest run"
61
+ },
59
62
  "devDependencies": {
60
63
  "@pinia/nuxt": "^0.11.3",
61
64
  "nuxt": "^4.4.2",
62
65
  "pinia": "^3.0.4",
63
66
  "pinia-plugin-persistedstate": "^4.7.1",
67
+ "vitest": "^2",
64
68
  "vue": "^3.5.0"
65
69
  }
66
70
  }
@@ -1,12 +1,9 @@
1
- // Registers the Authorization header interceptor into the shared registry.
2
- // Universal: useCookie reads request cookies in SSR and document.cookie on client.
3
1
  export default defineNuxtPlugin(() => {
4
2
  const { add } = useRequestInterceptors()
5
-
6
3
  add((headers: Record<string, string>, options: Record<string, unknown>) => {
7
- if (options.useToken !== false) {
8
- const token = useAuthStore().getToken()
9
- if (token) headers['Authorization'] = `Bearer ${token}`
10
- }
4
+ // Authorization lo inyecta el BFF (server) desde la sesión sellada.
5
+ // Aquí quedan solo headers no-sensibles (p. ej. multitenancy) si aplica.
6
+ void options
7
+ void headers
11
8
  })
12
9
  })
@@ -1,34 +1,23 @@
1
- // Universal plugin: runs on both server (SSR) and client.
2
- // On SSR: token is read from cookie → fetches user from API → pinia state is serialized
3
- // into the Nuxt payload and transferred to the client → no hydration mismatch.
4
- // Avoids useQueryClient() because vue-query.ts runs after this plugin alphabetically.
5
- // Universal plugin: runs on both server (SSR) and client.
6
- // On SSR: token is read from cookie → fetches user from API → pinia state is serialized
7
- // into the Nuxt payload and transferred to the client → no hydration mismatch.
8
- // Avoids useQueryClient() because vue-query.ts runs after this plugin alphabetically.
1
+ // Universal (server+client). Rehidrata la sesión sin tocar el token (vive en la
2
+ // cookie sellada, solo accesible por el BFF).
9
3
  export default defineNuxtPlugin(async () => {
10
4
  const authStore = useAuthStore()
11
-
12
- // Use getToken() not authStore.token — pinia-plugin-persistedstate may not have
13
- // hydrated the store yet in SSR, but getToken() reads useCookie() directly.
14
- if (!authStore.getToken()) return
15
-
16
- if (!authStore.isAuthenticated()) {
17
- authStore.logout()
18
- return
19
- }
20
-
21
- if (!authStore.user) {
22
- try {
23
- const api = useApi()
24
- const data = await api.get('auth/me')
25
- if (data) {
26
- authStore.saveUser(data.user ?? data)
27
- authStore.savePermissions(data.permissions ?? [])
28
- authStore.availableContexts = data.availableContexts ?? []
5
+ const api = useApi()
6
+ const requestFetch = useRequestFetch()
7
+ try {
8
+ const session = await requestFetch('/_auth/session')
9
+ if (!session?.authenticated) { authStore.setAuthenticated(false); return }
10
+ authStore.setAuthenticated(true)
11
+ if (session.currentContext) authStore.currentContext = session.currentContext
12
+ if (!authStore.user) {
13
+ const me = await api.get('auth/me')
14
+ if (me) {
15
+ authStore.saveUser(me.user ?? me)
16
+ authStore.savePermissions(me.permissions ?? [])
17
+ authStore.availableContexts = me.availableContexts ?? me.contexts ?? []
29
18
  }
30
- } catch {
31
- if (import.meta.client) authStore.logout()
32
19
  }
20
+ } catch {
21
+ authStore.setAuthenticated(false)
33
22
  }
34
23
  })
@@ -0,0 +1,13 @@
1
+ import { readSession } from '../utils/session'
2
+
3
+ // Lee la sesión sellada una vez por request y deja el token en event.context
4
+ // para que useApi (SSR) adjunte el Bearer en llamadas directas a Laravel.
5
+ // Defensivo: si no hay secret/sesión, sigue sin token (no revienta el request).
6
+ export default defineEventHandler(async (event) => {
7
+ try {
8
+ const { token } = await readSession(event)
9
+ if (token) event.context.authToken = token
10
+ } catch {
11
+ // sin NUXT_SESSION_PASSWORD o sin sesión válida → seguir sin token
12
+ }
13
+ })
@@ -0,0 +1,25 @@
1
+ import { defineEventHandler, getRouterParam, getQuery, sendRedirect, createError } from 'h3'
2
+ import { writeSession } from '../../../utils/session'
3
+ import { normalizeLoginResponse } from '../../../utils/login'
4
+
5
+ // Recibe el callback de OAuth, intercambia contra Laravel, sella la sesión y
6
+ // redirige a la app. El token nunca llega al browser.
7
+ export default defineEventHandler(async (event) => {
8
+ const provider = getRouterParam(event, 'provider')
9
+ const query = getQuery(event)
10
+ const config = useRuntimeConfig(event)
11
+ const context = (query.context as string) || 'backoffice'
12
+ let data: any
13
+ try {
14
+ data = await $fetch(`${config.apiInternalUrl}/api/auth/${provider}/callback`, {
15
+ method: 'GET', query, headers: { Accept: 'application/json' },
16
+ })
17
+ } catch (err: any) {
18
+ const msg = err?.data?.message ?? 'oauth_failed'
19
+ return sendRedirect(event, `${config.public.loginPath}?social_error=${encodeURIComponent(msg)}`)
20
+ }
21
+ const { token } = normalizeLoginResponse(data)
22
+ if (!token) throw createError({ statusCode: 502, statusMessage: 'oauth_no_token' })
23
+ await writeSession(event, { token, currentContext: context })
24
+ return sendRedirect(event, config.public.homePath)
25
+ })
@@ -0,0 +1,11 @@
1
+ import { defineEventHandler, readBody, createError } from 'h3'
2
+ import { readSession, writeSession } from '../../utils/session'
3
+
4
+ // Cambia el contexto activo en la sesión (el modal "¿seguro?" es UX de front).
5
+ export default defineEventHandler(async (event) => {
6
+ const { context } = await readBody(event)
7
+ const current = await readSession(event)
8
+ if (!current.token) throw createError({ statusCode: 401, statusMessage: 'no_session' })
9
+ await writeSession(event, { ...current, currentContext: context })
10
+ return { ok: true, currentContext: context }
11
+ })
@@ -0,0 +1,25 @@
1
+ import { defineEventHandler, readBody, createError } from 'h3'
2
+ import { writeSession } from '../../utils/session'
3
+ import { normalizeLoginResponse } from '../../utils/login'
4
+
5
+ // Llama a Laravel /auth/login server-to-server; en éxito sella el JWT en la
6
+ // sesión y devuelve el body SIN token. Propaga el status de Laravel (403 = el
7
+ // usuario no posee ese contexto → el probing del cliente prueba el siguiente).
8
+ export default defineEventHandler(async (event) => {
9
+ const { context, email, password, remember } = await readBody(event)
10
+ const config = useRuntimeConfig(event)
11
+ let data: any
12
+ try {
13
+ data = await $fetch(`${config.apiInternalUrl}/api/auth/login`, {
14
+ method: 'POST',
15
+ body: { context, email, password, remember },
16
+ headers: { Accept: 'application/json' },
17
+ })
18
+ } catch (err: any) {
19
+ const status = err?.response?.status ?? err?.statusCode ?? 500
20
+ throw createError({ statusCode: status, data: err?.data ?? null, statusMessage: 'login_failed' })
21
+ }
22
+ const { token, body } = normalizeLoginResponse(data)
23
+ await writeSession(event, { token, currentContext: context })
24
+ return body
25
+ })
@@ -0,0 +1,18 @@
1
+ import { defineEventHandler } from 'h3'
2
+ import { readSession, clearSession } from '../../utils/session'
3
+ import { buildAuthHeaders } from '../../utils/bff'
4
+
5
+ // Best-effort: avisa a Laravel con el Bearer de la sesión, luego limpia la sesión.
6
+ export default defineEventHandler(async (event) => {
7
+ const config = useRuntimeConfig(event)
8
+ const { token } = await readSession(event)
9
+ if (token) {
10
+ try {
11
+ await $fetch(`${config.apiInternalUrl}/api/auth/logout`, {
12
+ method: 'POST', headers: { ...buildAuthHeaders(token), Accept: 'application/json' },
13
+ })
14
+ } catch { /* best-effort */ }
15
+ }
16
+ await clearSession(event)
17
+ return { ok: true }
18
+ })
@@ -0,0 +1,8 @@
1
+ import { defineEventHandler } from 'h3'
2
+ import { readSession } from '../../utils/session'
3
+
4
+ // Para SSR/middlewares: ¿hay sesión? ¿cuál es el contexto activo? Sin exponer el token.
5
+ export default defineEventHandler(async (event) => {
6
+ const { token, currentContext } = await readSession(event)
7
+ return { authenticated: !!token, currentContext: currentContext ?? null }
8
+ })
@@ -0,0 +1,15 @@
1
+ import { defineEventHandler, proxyRequest, getRequestURL } from 'h3'
2
+ import { readSession } from '../../utils/session'
3
+ import { resolveApiTarget, buildAuthHeaders } from '../../utils/bff'
4
+
5
+ // BFF: reenvía /api/** a Laravel inyectando el Bearer desde la sesión sellada.
6
+ // proxyRequest hace passthrough de método/body/headers y STREAMEA la respuesta
7
+ // (crítico para el SSE del asistente). No se reenvía la cookie de sesión.
8
+ export default defineEventHandler(async (event) => {
9
+ const config = useRuntimeConfig(event)
10
+ const { token } = await readSession(event)
11
+ const path = (event.context.params?.path ?? '') as string
12
+ const search = getRequestURL(event).search
13
+ const target = resolveApiTarget(config.apiInternalUrl, path, search)
14
+ return proxyRequest(event, target, { headers: buildAuthHeaders(token) })
15
+ })
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { resolveApiTarget, buildAuthHeaders } from '../bff'
3
+
4
+ describe('resolveApiTarget', () => {
5
+ it('arma base + /api/ + path + querystring', () => {
6
+ expect(resolveApiTarget('http://api:80', 'auth/me', '')).toBe('http://api:80/api/auth/me')
7
+ expect(resolveApiTarget('http://api:80', 'question-bank', '?perPage=1'))
8
+ .toBe('http://api:80/api/question-bank?perPage=1')
9
+ })
10
+ it('normaliza slashes duplicados en la base', () => {
11
+ expect(resolveApiTarget('http://api:80/', 'auth/me', '')).toBe('http://api:80/api/auth/me')
12
+ })
13
+ })
14
+
15
+ describe('buildAuthHeaders', () => {
16
+ it('con token inyecta Authorization y vacía la cookie', () => {
17
+ expect(buildAuthHeaders('jwt123')).toEqual({ authorization: 'Bearer jwt123', cookie: '' })
18
+ })
19
+ it('sin token solo vacía la cookie (no manda Authorization)', () => {
20
+ expect(buildAuthHeaders(undefined)).toEqual({ cookie: '' })
21
+ })
22
+ })
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { normalizeLoginResponse } from '../login'
3
+
4
+ describe('normalizeLoginResponse', () => {
5
+ it('separa el token del resto', () => {
6
+ const r = normalizeLoginResponse({
7
+ token: 'jwt', user: { id: '1' }, contexts: ['backoffice'], requires_password_change: false,
8
+ })
9
+ expect(r.token).toBe('jwt')
10
+ expect(r.body).toEqual({ user: { id: '1' }, contexts: ['backoffice'], requires_password_change: false })
11
+ expect((r.body as any).token).toBeUndefined()
12
+ })
13
+ it('acepta access_token como alias', () => {
14
+ expect(normalizeLoginResponse({ access_token: 'jwt2' }).token).toBe('jwt2')
15
+ })
16
+ })
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { sessionOptions, SESSION_COOKIE } from '../session'
3
+
4
+ describe('sessionOptions', () => {
5
+ it('sella con el password dado y cookie httpOnly/lax/secure/maxAge', () => {
6
+ const o = sessionOptions('x'.repeat(32))
7
+ expect(o.password).toBe('x'.repeat(32))
8
+ expect(o.name).toBe(SESSION_COOKIE)
9
+ expect(o.cookie).toMatchObject({ httpOnly: true, sameSite: 'lax', secure: true, path: '/', maxAge: 60 * 60 * 24 * 7 })
10
+ })
11
+ it('permite secure:false (dev http)', () => {
12
+ expect(sessionOptions('x'.repeat(32), { secure: false }).cookie.secure).toBe(false)
13
+ })
14
+ it('lanza si el password es muy corto (<32)', () => {
15
+ expect(() => sessionOptions('corto')).toThrow(/32/)
16
+ })
17
+ })
@@ -0,0 +1,5 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ describe('vitest', () => {
4
+ it('runs', () => { expect(1 + 1).toBe(2) })
5
+ })
@@ -0,0 +1,16 @@
1
+ // Lógica pura del BFF. Sin dependencias de h3 para poder testear aislado.
2
+
3
+ /** Construye la URL destino en Laravel: base + /api/ + path + query. */
4
+ export function resolveApiTarget(base: string, path: string, search: string): string {
5
+ const cleanBase = base.replace(/\/+$/, '')
6
+ return `${cleanBase}/api/${path}${search}`
7
+ }
8
+
9
+ /**
10
+ * Headers a inyectar al reenviar a Laravel:
11
+ * - Authorization Bearer si hay token de sesión.
12
+ * - cookie vacía: NO reenviar la cookie de sesión de Nitro al backend.
13
+ */
14
+ export function buildAuthHeaders(token?: string): Record<string, string> {
15
+ return token ? { authorization: `Bearer ${token}`, cookie: '' } : { cookie: '' }
16
+ }
@@ -0,0 +1,21 @@
1
+ export interface LaravelLoginResponse {
2
+ token?: string
3
+ access_token?: string
4
+ user?: unknown
5
+ contexts?: string[]
6
+ requires_password_change?: boolean
7
+ [k: string]: unknown
8
+ }
9
+
10
+ /** Separa el token (va a la sesión) del body que vuelve al cliente (sin token). */
11
+ export function normalizeLoginResponse(data: LaravelLoginResponse) {
12
+ const token = data.token ?? data.access_token
13
+ return {
14
+ token,
15
+ body: {
16
+ user: data.user ?? null,
17
+ contexts: data.contexts ?? [],
18
+ requires_password_change: data.requires_password_change ?? false,
19
+ },
20
+ }
21
+ }
@@ -0,0 +1,53 @@
1
+ import { useSession } from 'h3'
2
+ import type { H3Event } from 'h3'
3
+
4
+ export const SESSION_COOKIE = 'innertia_session'
5
+
6
+ export interface SessionData {
7
+ token?: string
8
+ currentContext?: string
9
+ }
10
+
11
+ /** Opciones de la sesión sellada. Pura y testeable. */
12
+ export function sessionOptions(password: string, opts: { secure?: boolean } = {}) {
13
+ if (!password || password.length < 32) {
14
+ throw new Error('[innertia-auth] NUXT_SESSION_PASSWORD debe tener ≥32 caracteres')
15
+ }
16
+ return {
17
+ password,
18
+ name: SESSION_COOKIE,
19
+ cookie: {
20
+ httpOnly: true,
21
+ sameSite: 'lax' as const,
22
+ secure: opts.secure ?? true,
23
+ path: '/',
24
+ maxAge: 60 * 60 * 24 * 7, // 7 días
25
+ },
26
+ }
27
+ }
28
+
29
+ // Secure cookie en qa/prod (HTTPS); en dev local (http) debe ir sin Secure o el
30
+ // browser la descarta silenciosamente → logout loop.
31
+ function secureCookie(): boolean {
32
+ return process.env.NODE_ENV !== 'development'
33
+ }
34
+
35
+ function password(event: H3Event): string {
36
+ // runtimeConfig.session.password (privado), llenado desde NUXT_SESSION_PASSWORD.
37
+ return useRuntimeConfig(event).session.password
38
+ }
39
+
40
+ export async function readSession(event: H3Event): Promise<SessionData> {
41
+ const s = await useSession<SessionData>(event, sessionOptions(password(event), { secure: secureCookie() }))
42
+ return s.data
43
+ }
44
+
45
+ export async function writeSession(event: H3Event, data: SessionData): Promise<void> {
46
+ const s = await useSession<SessionData>(event, sessionOptions(password(event), { secure: secureCookie() }))
47
+ await s.update(data)
48
+ }
49
+
50
+ export async function clearSession(event: H3Event): Promise<void> {
51
+ const s = await useSession<SessionData>(event, sessionOptions(password(event), { secure: secureCookie() }))
52
+ await s.clear()
53
+ }
package/stores/auth.js CHANGED
@@ -18,19 +18,6 @@ function _setCookie(name, value, maxAgeSeconds) {
18
18
  }
19
19
  }
20
20
 
21
- function _getCookie(name) {
22
- try {
23
- const cookie = useCookie(name)
24
- return cookie.value ?? null
25
- } catch {
26
- if (import.meta.client) {
27
- const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
28
- return match ? decodeURIComponent(match[1]) : null
29
- }
30
- return null
31
- }
32
- }
33
-
34
21
  function _deleteCookie(name) {
35
22
  try {
36
23
  const cookie = useCookie(name)
@@ -42,20 +29,10 @@ function _deleteCookie(name) {
42
29
  }
43
30
  }
44
31
 
45
- // ─── JWT decode (no lib) ───────────────────────────────────────────────────
46
- function _decodeJwtExpiry(token) {
47
- try {
48
- const payload = JSON.parse(atob(token.split('.')[1]))
49
- return payload.exp ?? null
50
- } catch {
51
- return null
52
- }
53
- }
54
-
55
32
  // ─── store ─────────────────────────────────────────────────────────────────
56
33
  export const useAuthStore = defineStore('auth', {
57
34
  state: () => ({
58
- token: null,
35
+ authenticated: false,
59
36
  user: null,
60
37
  currentContext: null,
61
38
  availableContexts: [],
@@ -64,13 +41,6 @@ export const useAuthStore = defineStore('auth', {
64
41
  }),
65
42
 
66
43
  persist: [
67
- {
68
- // Token en cookie con key propio para que SSR lo lea en middlewares (guest/auth).
69
- // Key separado para evitar que la segunda config sobreescriba el token.
70
- key: 'auth_token',
71
- pick: ['token'],
72
- storage: piniaPluginPersistedstate.cookies,
73
- },
74
44
  {
75
45
  // currentContext y availableContexts en localStorage.
76
46
  // user NO se persiste: auth-init.client.ts lo recarga desde API en cada boot,
@@ -82,22 +52,13 @@ export const useAuthStore = defineStore('auth', {
82
52
  ],
83
53
 
84
54
  actions: {
85
- // ── token ──────────────────────────────────────────────────────────────
86
- saveToken(token) {
87
- this.token = token
88
- _setCookie('auth_token', token, 60 * 60 * 24 * 7) // 7 days
89
- },
90
-
91
- getToken() {
92
- return this.token ?? _getCookie('auth_token')
55
+ // ── auth flag ──────────────────────────────────────────────────────────
56
+ // El token ya no vive en el cliente. isAuthenticated se resuelve por /_auth/session.
57
+ setAuthenticated(value) {
58
+ this.authenticated = !!value
93
59
  },
94
-
95
60
  isAuthenticated() {
96
- const token = this.getToken()
97
- if (!token) return false
98
- const exp = _decodeJwtExpiry(token)
99
- if (exp === null) return true // non-JWT or no expiry claim → treat as valid
100
- return Date.now() / 1000 < exp
61
+ return this.authenticated === true
101
62
  },
102
63
 
103
64
  // ── user ───────────────────────────────────────────────────────────────
@@ -118,12 +79,11 @@ export const useAuthStore = defineStore('auth', {
118
79
 
119
80
  // ── logout ─────────────────────────────────────────────────────────────
120
81
  logout() {
121
- this.token = null
82
+ this.authenticated = false
122
83
  this.user = null
123
84
  this.currentContext = null
124
85
  this.availableContexts = []
125
86
  this.permissions = []
126
- _deleteCookie('auth_token')
127
87
  _deleteCookie('auth_user')
128
88
  },
129
89
  },
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['**/__tests__/**/*.test.ts', '**/*.test.ts'],
6
+ environment: 'node',
7
+ },
8
+ })