From 011cdb03234f62cbb1aab4f88441ba82bd248147 Mon Sep 17 00:00:00 2001 From: Beldor Date: Thu, 18 Jun 2026 18:03:13 -0300 Subject: [PATCH] feat: migrar emails de Resend a Nodemailer + Gmail SMTP Reemplaza Resend por nodemailer con Gmail SMTP. El to: ahora usa el email real del cliente en lugar del hardcodeado de sandbox. Variables de entorno: GMAIL_USER y GMAIL_APP_PASSWORD. Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 3 +- package-lock.json | 72 +++++++++++++------------------------------ package.json | 5 +-- src/lib/magic-link.ts | 21 ++++++++----- 4 files changed, 40 insertions(+), 61 deletions(-) diff --git a/.env.example b/.env.example index 6493c96..5da1ef6 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ DATABASE_URL="postgresql://taller:taller123@localhost:5432/taller_turnos" NEXTAUTH_SECRET="cambia-esto-por-un-secret-seguro" NEXTAUTH_URL="http://localhost:3000" -RESEND_API_KEY="re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +GMAIL_USER="tu-email@gmail.com" +GMAIL_APP_PASSWORD="xxxx xxxx xxxx xxxx" MAGIC_LINK_BASE_URL="http://localhost:3000" diff --git a/package-lock.json b/package-lock.json index c534ade..320d7d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,16 @@ "dependencies": { "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "@types/nodemailer": "^8.0.1", "bcryptjs": "^3.0.3", "jose": "^6.2.3", "jsonwebtoken": "^9.0.3", "next": "14.2.35", "next-auth": "^5.0.0-beta.25", + "nodemailer": "^6.10.1", "prisma": "^7.8.0", "react": "^18", - "react-dom": "^18", - "resend": "^6.14.0" + "react-dom": "^18" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", @@ -1416,12 +1417,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1502,6 +1497,15 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", @@ -3884,12 +3888,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, "node_modules/fast-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", @@ -5622,6 +5620,15 @@ "semver": "bin/semver.js" } }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6094,12 +6101,6 @@ "node": ">= 0.4" } }, - "node_modules/postal-mime": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", - "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", - "license": "MIT-0" - }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -6611,27 +6612,6 @@ "node": ">=0.10.0" } }, - "node_modules/resend": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-6.14.0.tgz", - "integrity": "sha512-jVdpUgOoWGLjaP64lo8KwzHT9gY4w6Dl8c36CIb2F+ayYOMLr3khqs8xrNjXM2k19b+lPoj0VWQFhVNLiToBjA==", - "license": "MIT", - "dependencies": { - "postal-mime": "2.7.4", - "standardwebhooks": "1.0.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@react-email/render": "*" - }, - "peerDependenciesMeta": { - "@react-email/render": { - "optional": true - } - } - }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -7058,16 +7038,6 @@ "dev": true, "license": "MIT" }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", diff --git a/package.json b/package.json index d1de675..1ad4da1 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,16 @@ "dependencies": { "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "@types/nodemailer": "^8.0.1", "bcryptjs": "^3.0.3", "jose": "^6.2.3", "jsonwebtoken": "^9.0.3", "next": "14.2.35", "next-auth": "^5.0.0-beta.25", + "nodemailer": "^6.10.1", "prisma": "^7.8.0", "react": "^18", - "react-dom": "^18", - "resend": "^6.14.0" + "react-dom": "^18" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", diff --git a/src/lib/magic-link.ts b/src/lib/magic-link.ts index 00d365f..fa0601b 100644 --- a/src/lib/magic-link.ts +++ b/src/lib/magic-link.ts @@ -1,21 +1,28 @@ import { prisma } from "./prisma"; -import { Resend } from "resend"; +import nodemailer from "nodemailer"; import crypto from "crypto"; -const resend = new Resend(process.env.RESEND_API_KEY); +const transporter = nodemailer.createTransport({ + host: "smtp.gmail.com", + port: 587, + secure: false, + auth: { + user: process.env.GMAIL_USER, + pass: process.env.GMAIL_APP_PASSWORD, + }, +}); export async function enviarMagicLink(email: string): Promise<{ ok: boolean; error?: string }> { const cliente = await prisma.cliente.findUnique({ where: { email } }); if (!cliente) return { ok: false, error: "Email no registrado" }; - // Invalidar tokens anteriores no usados await prisma.magicToken.updateMany({ where: { clienteId: cliente.id, usado: false }, data: { usado: true }, }); const token = crypto.randomBytes(32).toString("hex"); - const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutos + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); await prisma.magicToken.create({ data: { clienteId: cliente.id, token, expiresAt }, @@ -23,9 +30,9 @@ export async function enviarMagicLink(email: string): Promise<{ ok: boolean; err const link = `${process.env.MAGIC_LINK_BASE_URL}/cliente/auth?token=${token}`; - await resend.emails.send({ - from: "Taller ", - to: "jpbeldomenico@gmail.com", + await transporter.sendMail({ + from: `"Taller" <${process.env.GMAIL_USER}>`, + to: email, subject: "Tu link de acceso al taller", html: `

Hola ${cliente.nombre}!