@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.
|
|
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
|
+
})
|
package/server/utils/session.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
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
|
|
42
|
-
return
|
|
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
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
await s.clear()
|
|
79
|
+
deleteCookie(event, SESSION_COOKIE, { path: '/' })
|
|
53
80
|
}
|