Ecosistema LPDI · LAP-329 / LAP-340 · 2026-06-01
Esta página responde al ajuste de Frank sobre EMAIL-003 (Contacto Welcome) y EMAIL-004 (CEO Welcome). El inventario reportó al revés. La validación de TyC para el contacto/CEO nuevo ya está implementada y en producción desde el 2026-05-26 (commit eaa42f2, LAP-325) y reforzada el 2026-05-30 con guard global (0bef598).
| ID | CTA destino | Inventario decía | Realidad | |
|---|---|---|---|---|
| EMAIL-003 | Contacto Welcome — usuario nuevo, ICG nuevo | /auth/reset-password | NO | SI — obligatorio |
| EMAIL-003.b | Contacto Welcome — usuario nuevo, ICG existente | /auth/reset-password | NO | SI — obligatorio |
| EMAIL-003.c | Contacto Welcome — usuario existente, ICG nuevo | /dashboard | NO | SI — guard global |
| EMAIL-003.d | Contacto Welcome — usuario existente, ICG existente | /dashboard | NO | SI — guard global |
| EMAIL-004 | CEO Welcome — usuario nuevo | /auth/reset-password | NO | SI — obligatorio |
| EMAIL-004.b | CEO Welcome — usuario existente | /dashboard | NO | SI — guard global |
Caso A — Contacto/CEO nuevo recibe credenciales temporales:
consent_log con el user_id de la sesion recien creada por el token de recovery.count(consent_log WHERE user_id = X) === 0 → flag needsConsent: true viaja al cliente.disabled hasta que ambos checkboxes esten marcados Y la contrasena cumpla criterios.POST /api/auth/accept-consent que graba dos filas en consent_log (terms + privacy, metadata flow: 'password_reset').consent_log queda con user_id = el del contacto/CEO.Caso B — Contacto/CEO ya existia en el sistema (boton "Ver mi registro" → /dashboard):
dashboard/+layout.server.ts consulta consent_log antes de renderizar.count === 0 → redirect(303, '/aceptar-terminos')./aceptar-terminos tiene los mismos dos checkboxes obligatorios y bloquea el boton hasta marcar ambos./api/auth/accept-consent con flow: 'first_login' y redirect al dashboard.src/routes/auth/reset-password/+page.server.ts LAP-325
// LAP-325: si el usuario llego via welcome email (CEO/contacto ICG creado por // otra persona) y nunca acepto T&C / Privacidad, mostrar checkboxes obligatorios // junto al cambio de contrasena. Detectamos esto mirando si tiene filas en // consent_log: cero filas = primer ingreso, requerir aceptacion. export const load: PageServerLoad = async ({ locals, url }) => { const { user } = await locals.safeGetSession(); if (!user) throw redirect(303, '/login?tab=recover&error=expired'); const { count } = await supabaseAdmin .from('consent_log') .select('id', { count: 'exact', head: true }) .eq('user_id', user.id); return { needsConsent: (count ?? 0) === 0 }; };
src/routes/auth/reset-password/+page.svelte
let termsAccepted = false; let privacyAccepted = false; // reactive: consent OK solo si no se requiere O si ambos checkbox marcados $: consentOk = !data.needsConsent || (termsAccepted && privacyAccepted); // El handler valida explicitamente antes de submit if (data.needsConsent && (!termsAccepted || !privacyAccepted)) { message = 'Debes aceptar los Términos y la Política de Tratamiento de Datos.'; return; } // Boton: disabled hasta cumplir password + consent <button type="submit" disabled={loading || !allCriteriaMet || !passwordsMatch || !consentOk}> {loading ? 'Actualizando...' : 'Establecer nueva contraseña'} </button>
src/routes/dashboard/+layout.server.ts:43-53
// H1: Guard de consent — usuarios sin ningun registro en consent_log no han // aceptado T&C de forma explicita (CEO designado / contacto ICG creados por // otra persona). Redirigir a pantalla de aceptacion antes de entrar al dashboard. const { count: consentCount } = await supabaseAdmin .from('consent_log') .select('id', { count: 'exact', head: true }) .eq('user_id', user.id); if ((consentCount ?? 0) === 0) { throw redirect(303, '/aceptar-terminos'); }
src/routes/api/auth/accept-consent/+server.ts
export const POST: RequestHandler = async (event) => { const { user } = await event.locals.safeGetSession(); if (!user) throw error(401, 'No autenticado'); if (!body.terms || !body.privacy) { return json({ error: 'Debes aceptar Términos y Política de Privacidad.' }, { status: 400 }); } logConsent(event, [ { userId: user.id, consentType: 'terms', metadata: { flow: body.flow ?? 'password_reset' } }, { userId: user.id, consentType: 'privacy', metadata: { flow: body.flow ?? 'password_reset' } }, ]); return json({ ok: true }); };
Asi se ve la pantalla destino del CTA cuando el contacto/CEO nuevo abre el correo. Replicado fielmente desde auth/reset-password/+page.svelte con needsConsent: true (caso productivo de un destinatario sin filas previas en consent_log).
Botón deshabilitado hasta marcar ambos checkboxes
disabled (opacity 0.5, cursor not-allowed) y el server rechaza el submit aun si el cliente fuera bypaseado.eaa42f2 · feat(LAP-325): consent obligatorio en reset-password para usuarios sin historial · 2026-05-26 14:06 -05
0bef598 · feat(consent): TyC obligatorios en invitación equipo + guard primer login (C5/H2 + H1) · 2026-05-30 12:06 -05
Ambos commits estan en main, desplegados en eco.lpdi.co. Branch: master. Origin: devlapuntadeliceberg/relacionamiento.
Ecosistema LPDI · pmo_agent · LAP-329 evidencia · 2026-06-01