Jelajahi Sumber

implement data consent form

Matthew Trejo 6 hari lalu
induk
melakukan
79f655d84b

+ 19 - 0
docs/DATA_PROCESSING_CONSENT.md

@@ -0,0 +1,19 @@
+# Implementación de Consentimiento de Datos
+
+- [x] **Base de Datos**
+  - [x] Agregar `dataProcessingConsent` (Boolean) y `dataProcessingConsentDate` (DateTime) al modelo `User` en `schema.prisma`.
+  - [ ] Ejecutar migración.
+
+- [x] **Tipos y Autenticación**
+  - [x] Actualizar `src/types/next-auth.d.ts` para incluir `dataProcessingConsent`.
+  - [x] Actualizar `src/lib/auth.ts` (`authorize` y `session` callback) para propagar el campo.
+
+- [x] **API**
+  - [x] Crear endpoint `src/app/api/users/consent/route.ts` para actualizar el consentimiento.
+
+- [x] **Frontend**
+  - [x] Crear componente `DataConsentModal`.
+  - [x] Integrar en `AuthenticatedLayout.tsx`.
+  - [x] Lógica: Si `!session.user.dataProcessingConsent` -> Mostrar modal.
+  - [x] Acción Aceptar: API call + `update()` sesión.
+  - [x] Acción Rechazar: `signOut()`.

+ 3 - 0
prisma/migrations/20251212163634_add_data_consent/migration.sql

@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN     "dataProcessingConsent" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN     "dataProcessingConsentDate" TIMESTAMP(3);

+ 4 - 0
prisma/schema.prisma

@@ -31,6 +31,10 @@ model User {
   updatedAt    DateTime @updatedAt
   deletedAt    DateTime?
   
+  // Consentimiento de datos
+  dataProcessingConsent Boolean @default(false)
+  dataProcessingConsentDate DateTime?
+  
   // Relaciones
   records      Record[]
   assignedPatients PatientAssignment[] @relation("DoctorPatients")

+ 42 - 0
src/app/api/users/consent/route.ts

@@ -0,0 +1,42 @@
+import { NextResponse } from "next/server";
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/lib/auth";
+import { prisma } from "@/lib/prisma";
+
+export async function POST(req: Request) {
+  try {
+    const session = await getServerSession(authOptions);
+
+    if (!session?.user?.id) {
+      return NextResponse.json(
+        { error: "No autorizado" },
+        { status: 401 }
+      );
+    }
+
+    // Actualizar el consentimiento del usuario
+    const user = await prisma.user.update({
+      where: {
+        id: session.user.id,
+      },
+      data: {
+        dataProcessingConsent: true,
+        dataProcessingConsentDate: new Date(),
+      },
+    });
+
+    return NextResponse.json({ 
+      success: true, 
+      message: "Consentimiento registrado correctamente",
+      user: {
+        dataProcessingConsent: user.dataProcessingConsent
+      }
+    });
+  } catch (error) {
+    console.error("Error al registrar consentimiento:", error);
+    return NextResponse.json(
+      { error: "Error interno del servidor" },
+      { status: 500 }
+    );
+  }
+}

+ 2 - 0
src/components/AuthenticatedLayout.tsx

@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"
 import { useEffect, useState } from "react"
 import Sidebar from "@/components/Sidebar"
 import Footer from "@/components/Footer"
+import { DataConsentModal } from "@/components/DataConsentModal"
 import { COLOR_PALETTE } from "@/utils/palette"
 
 interface AuthenticatedLayoutProps {
@@ -51,6 +52,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
 
   return (
     <div className="min-h-screen" style={{ backgroundColor: COLOR_PALETTE.gray[50] }}>
+      <DataConsentModal />
       <Sidebar 
         isCollapsed={isSidebarCollapsed} 
         onToggleCollapse={() => setIsSidebarCollapsed(!isSidebarCollapsed)} 

+ 107 - 0
src/components/DataConsentModal.tsx

@@ -0,0 +1,107 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useSession, signOut } from "next-auth/react"
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { toast } from "sonner"
+
+export function DataConsentModal() {
+  const { data: session, update } = useSession()
+  const [isOpen, setIsOpen] = useState(false)
+  const [isLoading, setIsLoading] = useState(false)
+
+  useEffect(() => {
+    // Si hay sesión, no está cargando, y el usuario NO ha dado consentimiento
+    if (session?.user && session.user.dataProcessingConsent === false) {
+      setIsOpen(true)
+    } else {
+      setIsOpen(false)
+    }
+  }, [session])
+
+  const handleAccept = async (e: React.MouseEvent) => {
+    e.preventDefault() // Prevenir cierre automático
+    setIsLoading(true)
+
+    try {
+      const response = await fetch("/api/users/consent", {
+        method: "POST",
+      })
+
+      if (!response.ok) {
+        throw new Error("Error al procesar la solicitud")
+      }
+
+      // Actualizar la sesión localmente para reflejar el cambio sin recargar
+      await update({
+        ...session,
+        user: {
+          ...session?.user,
+          dataProcessingConsent: true
+        }
+      })
+      
+      toast.success("Consentimiento registrado correctamente")
+      setIsOpen(false)
+    } catch (error) {
+      console.error(error)
+      toast.error("Hubo un error al guardar tu respuesta. Por favor intenta de nuevo.")
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  const handleReject = async (e: React.MouseEvent) => {
+    e.preventDefault()
+    setIsLoading(true)
+    
+    try {
+      await signOut({ callbackUrl: "/auth/login" })
+    } catch (error) {
+      console.error(error)
+      setIsLoading(false)
+    }
+  }
+
+  if (!session) return null
+
+  return (
+    <AlertDialog open={isOpen}>
+      <AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
+        <AlertDialogHeader>
+          <AlertDialogTitle>Consentimiento de Tratamiento de Datos</AlertDialogTitle>
+          <AlertDialogDescription asChild className="space-y-4 text-left">
+            <div>
+              <p>
+                Para continuar utilizando nuestros servicios, necesitamos tu consentimiento para el tratamiento de tus datos personales.
+              </p>
+              <p>
+                Tus datos serán utilizados exclusivamente para fines médicos y de seguimiento de tu tratamiento, garantizando su confidencialidad y seguridad de acuerdo con las normativas vigentes.
+              </p>
+              <p className="text-sm text-muted-foreground">
+                Si no aceptas, se cerrará tu sesión automáticamente.
+              </p>
+            </div>
+          </AlertDialogDescription>
+        </AlertDialogHeader>
+        <AlertDialogFooter>
+          <AlertDialogCancel onClick={handleReject} disabled={isLoading}>
+            No acepto (Salir)
+          </AlertDialogCancel>
+          <AlertDialogAction onClick={handleAccept} disabled={isLoading}>
+            {isLoading ? "Procesando..." : "Acepto el tratamiento de datos"}
+          </AlertDialogAction>
+        </AlertDialogFooter>
+      </AlertDialogContent>
+    </AlertDialog>
+  )
+}

+ 1 - 1
src/components/landing/Header.tsx

@@ -6,7 +6,7 @@ import { Stethoscope } from "lucide-react";
 
 export default function Header() {
   return (
-    <header className="bg-card shadow-sm border-b border-border">
+    <header className="sticky top-0 z-50 w-full bg-card shadow-sm border-b border-border">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
         <div className="flex justify-between items-center h-16">
           <div className="flex items-center space-x-2">

+ 3 - 0
src/lib/auth.ts

@@ -92,6 +92,7 @@ export const authOptions: NextAuthOptions = {
             medicalHistory: user.medicalHistory || undefined,
             allergies: user.allergies || undefined,
             currentMedications: user.currentMedications || undefined,
+            dataProcessingConsent: user.dataProcessingConsent,
           };
         } catch (error) {
           // Si no es error de "usar autenticación local", fallar
@@ -146,6 +147,7 @@ export const authOptions: NextAuthOptions = {
             medicalHistory: user.medicalHistory || undefined,
             allergies: user.allergies || undefined,
             currentMedications: user.currentMedications || undefined,
+            dataProcessingConsent: user.dataProcessingConsent,
           };
         } catch (error) {
           console.error("Error en autenticación local:", error);
@@ -196,6 +198,7 @@ export const authOptions: NextAuthOptions = {
           emergencyContact: token.emergencyContact as string | undefined,
           medicalHistory: token.medicalHistory as string | undefined,
           allergies: token.allergies as string | undefined,
+          dataProcessingConsent: token.dataProcessingConsent as boolean | undefined,
           currentMedications: token.currentMedications as string | undefined,
         };
       }

+ 2 - 0
src/types/next-auth.d.ts

@@ -20,6 +20,7 @@ declare module "next-auth" {
       medicalHistory?: string
       allergies?: string
       currentMedications?: string
+      dataProcessingConsent?: boolean
     }
   }
 
@@ -40,6 +41,7 @@ declare module "next-auth" {
     medicalHistory?: string
     allergies?: string
     currentMedications?: string
+    dataProcessingConsent?: boolean
   }
 }