@innertia-solutions/innertia-nuxt 0.1.61 → 0.1.63
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/components/Kanban/Standard.vue +24 -6
- package/package.json +3 -2
- package/server/utils/session.ts +42 -15
|
@@ -459,21 +459,37 @@ const onDragStart = (row, state) => {
|
|
|
459
459
|
}
|
|
460
460
|
const onDragOver = (e, state) => {
|
|
461
461
|
e.preventDefault()
|
|
462
|
-
dragOverState.value = state
|
|
462
|
+
if (dragOverState.value !== state) dragOverState.value = state
|
|
463
463
|
if (!props.reorderEndpoint) return
|
|
464
464
|
if (dragOverIndex.value === null) dragOverIndex.value = columnRows.value[state]?.length ?? 0
|
|
465
465
|
}
|
|
466
|
-
|
|
466
|
+
// Parpadeo: el drag nativo dispara `dragleave` al entrar a un hijo (tarjeta/placeholder).
|
|
467
|
+
// Si limpiamos el estado ahí, el fondo de la columna + el placeholder parpadean. Solo
|
|
468
|
+
// limpiamos cuando el cursor sale de verdad de la columna (relatedTarget fuera).
|
|
469
|
+
const onDragLeave = (e) => {
|
|
470
|
+
if (e.currentTarget.contains(e.relatedTarget)) return
|
|
471
|
+
dragOverState.value = null
|
|
472
|
+
dragOverIndex.value = null
|
|
473
|
+
}
|
|
474
|
+
// Al terminar el arrastre (drop o cancelado) limpiar siempre el estado visual.
|
|
475
|
+
const onDragEnd = () => {
|
|
476
|
+
dragOverState.value = null
|
|
477
|
+
dragOverIndex.value = null
|
|
478
|
+
draggedId.value = null
|
|
479
|
+
draggedFrom.value = null
|
|
480
|
+
}
|
|
467
481
|
|
|
468
482
|
// Llamado desde cada tarjeta: calcula si el cursor está en la mitad superior o inferior.
|
|
483
|
+
// Solo escribe el índice si cambió (evita reactividad redundante = menos re-render/parpadeo).
|
|
469
484
|
const onCardDragOver = (e, state, index) => {
|
|
470
485
|
if (!props.reorderEndpoint) return
|
|
471
486
|
e.preventDefault()
|
|
472
487
|
e.stopPropagation()
|
|
473
|
-
dragOverState.value = state
|
|
488
|
+
if (dragOverState.value !== state) dragOverState.value = state
|
|
474
489
|
const rect = e.currentTarget.getBoundingClientRect()
|
|
475
490
|
const after = (e.clientY - rect.top) > rect.height / 2
|
|
476
|
-
|
|
491
|
+
const next = after ? index + 1 : index
|
|
492
|
+
if (dragOverIndex.value !== next) dragOverIndex.value = next
|
|
477
493
|
}
|
|
478
494
|
|
|
479
495
|
const onDrop = async (targetState) => {
|
|
@@ -827,15 +843,17 @@ defineExpose({ reload: fetchAll, rows })
|
|
|
827
843
|
<template v-for="(row, ci) in columnRows[state.key]" :key="row.id">
|
|
828
844
|
<div
|
|
829
845
|
v-if="reorderEndpoint && dragOverState === state.key && dragOverIndex === ci"
|
|
830
|
-
class="rounded-card border-2 border-dashed border-primary/50 bg-primary/5 h-10"
|
|
846
|
+
class="rounded-card border-2 border-dashed border-primary/50 bg-primary/5 h-10 pointer-events-none"
|
|
831
847
|
/>
|
|
832
848
|
<div
|
|
833
849
|
draggable="true"
|
|
834
850
|
@dragstart="onDragStart(row, state.key)"
|
|
835
851
|
@dragover="onCardDragOver($event, state.key, ci)"
|
|
852
|
+
@dragend="onDragEnd"
|
|
836
853
|
@click="handleCardClick(row)"
|
|
837
|
-
class="bg-card border border-card-line rounded-card p-3 cursor-grab active:cursor-grabbing
|
|
854
|
+
class="bg-card border border-card-line rounded-card p-3 cursor-grab active:cursor-grabbing transition-colors select-none"
|
|
838
855
|
:class="[
|
|
856
|
+
draggedId ? '' : 'hover:border-primary/40 hover:shadow-sm',
|
|
839
857
|
draggedId === row.id ? 'opacity-40' : '',
|
|
840
858
|
previewRow?.id === row.id ? 'ring-2 ring-primary/30' : '',
|
|
841
859
|
]"
|
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.63",
|
|
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"
|
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
|
}
|