@innertia-solutions/innertia-nuxt 0.1.62 → 0.1.64

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/innertia-nuxt",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "description": "Innertia Solutions — Nuxt layer unificada: core, auth, multitenancy, theme y app contexts",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -54,7 +54,8 @@
54
54
  "tailwindcss": "^4.0.0",
55
55
  "uuid": "^13.0.0",
56
56
  "vanilla-calendar-pro": "^3.1.0",
57
- "vue3-apexcharts": "^1.11.1"
57
+ "vue3-apexcharts": "^1.11.1",
58
+ "iron-webcrypto": "^1.2.1"
58
59
  },
59
60
  "scripts": {
60
61
  "test": "vitest run"
@@ -0,0 +1,27 @@
1
+ import { defineEventHandler, readBody, createError } from 'h3'
2
+ import { writeSession } from '../../../utils/session'
3
+ import { normalizeLoginResponse } from '../../../utils/login'
4
+
5
+ // Login de plataforma (identidad separada, sin contexto de tenant). Llama a
6
+ // Laravel /platform/auth/login server-to-server; en éxito sella el JWT en la
7
+ // sesión httpOnly (igual que /_auth/login) y devuelve el body SIN token. Es
8
+ // imprescindible que pase por acá y no por el BFF catch-all /api/**, que solo
9
+ // proxea y NO sella la cookie → la sesión nunca quedaría establecida.
10
+ export default defineEventHandler(async (event) => {
11
+ const { email, password } = await readBody(event)
12
+ const config = useRuntimeConfig(event)
13
+ let data: any
14
+ try {
15
+ data = await $fetch(`${config.apiInternalUrl}/platform/auth/login`, {
16
+ method: 'POST',
17
+ body: { email, password },
18
+ headers: { Accept: 'application/json' },
19
+ })
20
+ } catch (err: any) {
21
+ const status = err?.response?.status ?? err?.statusCode ?? 500
22
+ throw createError({ statusCode: status, data: err?.data ?? null, statusMessage: 'login_failed' })
23
+ }
24
+ const { token, body } = normalizeLoginResponse(data)
25
+ await writeSession(event, { token })
26
+ return body
27
+ })
@@ -1,5 +1,15 @@
1
- import { useSession } from 'h3'
1
+ import { getCookie, setCookie, deleteCookie } from 'h3'
2
2
  import type { H3Event } from 'h3'
3
+ import { seal, unseal, defaults as ironDefaults } from 'iron-webcrypto'
4
+
5
+ // Sesión httpOnly sellada con iron (el token nunca llega al JS del browser).
6
+ //
7
+ // IMPORTANTE: NO usamos `useSession` de h3. Su `getSession` bajo la concurrencia de
8
+ // Nitro devolvía sesión vacía en ~60% de requests paralelos y, peor, REESCRIBÍA la
9
+ // cookie vacía cuando el unseal fallaba (envenenaba la sesión → logout aleatorio).
10
+ // iron (seal/unseal) es concurrency-safe por sí solo (probado 40× paralelo sin fallos);
11
+ // el bug vivía en la maquinaria de useSession. Acá hacemos lectura PURA: getCookie +
12
+ // unseal, sin tocar la cookie en fallo. Escritura explícita solo en login/logout/context.
3
13
 
4
14
  export const SESSION_COOKIE = 'innertia_session'
5
15
 
@@ -8,7 +18,7 @@ export interface SessionData {
8
18
  currentContext?: string
9
19
  }
10
20
 
11
- /** Opciones de la sesión sellada. Pura y testeable. */
21
+ /** Opciones de cookie sellada. `sessionOptions` se conserva para los tests (pura). */
12
22
  export function sessionOptions(password: string, opts: { secure?: boolean } = {}) {
13
23
  if (!password || password.length < 32) {
14
24
  throw new Error('[innertia-auth] NUXT_SESSION_PASSWORD debe tener ≥32 caracteres')
@@ -26,28 +36,45 @@ export function sessionOptions(password: string, opts: { secure?: boolean } = {}
26
36
  }
27
37
  }
28
38
 
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'
39
+ function password(event: H3Event): string {
40
+ const p = useRuntimeConfig(event).session.password
41
+ if (!p || p.length < 32) {
42
+ throw new Error('[innertia-auth] NUXT_SESSION_PASSWORD debe tener ≥32 caracteres')
43
+ }
44
+ return p
33
45
  }
34
46
 
35
- function password(event: H3Event): string {
36
- // runtimeConfig.session.password (privado), llenado desde NUXT_SESSION_PASSWORD.
37
- return useRuntimeConfig(event).session.password
47
+ // Secure en qa/prod (HTTPS); en dev local (http) sin Secure o el browser la descarta.
48
+ function secureCookie(): boolean {
49
+ return process.env.NODE_ENV !== 'development'
38
50
  }
39
51
 
52
+ /** Lectura PURA: getCookie + iron unseal. Nunca reescribe la cookie (ni en fallo). */
40
53
  export async function readSession(event: H3Event): Promise<SessionData> {
41
- const s = await useSession<SessionData>(event, sessionOptions(password(event), { secure: secureCookie() }))
42
- return s.data
54
+ const raw = getCookie(event, SESSION_COOKIE)
55
+ if (!raw) return {}
56
+ try {
57
+ const d = (await unseal(globalThis.crypto as Crypto, raw, password(event), ironDefaults)) as any
58
+ if (!d || typeof d !== 'object') return {}
59
+ // Compat: sesiones viejas selladas por useSession envuelven en {id,createdAt,data}.
60
+ return (d.data && typeof d.data === 'object') ? d.data : d
61
+ } catch {
62
+ return {}
63
+ }
43
64
  }
44
65
 
66
+ /** Sella {token, currentContext} y setea la cookie httpOnly. */
45
67
  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)
68
+ const sealed = await seal(globalThis.crypto as Crypto, data, password(event), ironDefaults)
69
+ setCookie(event, SESSION_COOKIE, sealed, {
70
+ httpOnly: true,
71
+ sameSite: 'lax',
72
+ secure: secureCookie(),
73
+ path: '/',
74
+ maxAge: 60 * 60 * 24 * 7,
75
+ })
48
76
  }
49
77
 
50
78
  export async function clearSession(event: H3Event): Promise<void> {
51
- const s = await useSession<SessionData>(event, sessionOptions(password(event), { secure: secureCookie() }))
52
- await s.clear()
79
+ deleteCookie(event, SESSION_COOKIE, { path: '/' })
53
80
  }