@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.
@@ -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
- const onDragLeave = () => { dragOverState.value = null; dragOverIndex.value = null }
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
- dragOverIndex.value = after ? index + 1 : index
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 hover:border-primary/40 hover:shadow-sm transition-all select-none"
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.61",
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"
@@ -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
  }