From 210fb26ec18ed381256486e42b3c0689e492d52f Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Sat, 14 Feb 2026 19:10:45 +0000 Subject: [PATCH] fix(BUG-021): remove verification code from API response, send via email - Replace Resend email service with nodemailer via local postfix relay - Remove code field from POST /v1/signup/free response - Send 6-digit verification code via email only (noreply@docfast.dev) - Add extra_hosts for Docker-to-host SMTP relay - Fire-and-forget email sending to avoid blocking API response --- docker-compose.yml | 5 +++- package-lock.json | 19 +++++++++++++ package.json | 2 ++ src/routes/signup.ts | 15 +++++------ src/services/email.ts | 63 +++++++++++++------------------------------ 5 files changed, 50 insertions(+), 54 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index af57b32..552715c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: restart: unless-stopped ports: - "127.0.0.1:3100:3100" + extra_hosts: + - "host.docker.internal:host-gateway" environment: - API_KEYS=${API_KEYS} - PORT=3100 @@ -13,7 +15,8 @@ services: - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - BASE_URL=${BASE_URL:-https://docfast.dev} - PRO_KEYS=${PRO_KEYS} - - RESEND_API_KEY=${RESEND_API_KEY:-FILL_IN} + - SMTP_HOST=host.docker.internal + - SMTP_PORT=25 volumes: - docfast-data:/app/data mem_limit: 512m diff --git a/package-lock.json b/package-lock.json index 5f01fb9..1993739 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,14 @@ "helmet": "^8.0.0", "marked": "^15.0.0", "nanoid": "^5.0.0", + "nodemailer": "^8.0.1", "puppeteer": "^24.0.0", "stripe": "^20.3.1" }, "devDependencies": { "@types/express": "^5.0.0", "@types/node": "^22.0.0", + "@types/nodemailer": "^7.0.9", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^3.0.0" @@ -984,6 +986,15 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -2572,6 +2583,14 @@ "node": ">= 0.4.0" } }, + "node_modules/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index 2d6260a..6bb2ff8 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "helmet": "^8.0.0", "marked": "^15.0.0", "nanoid": "^5.0.0", + "nodemailer": "^8.0.1", "puppeteer": "^24.0.0", "stripe": "^20.3.1" }, "devDependencies": { "@types/express": "^5.0.0", "@types/node": "^22.0.0", + "@types/nodemailer": "^7.0.9", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^3.0.0" diff --git a/src/routes/signup.ts b/src/routes/signup.ts index 1e03f67..de4f2e8 100644 --- a/src/routes/signup.ts +++ b/src/routes/signup.ts @@ -22,7 +22,6 @@ const verifyLimiter = rateLimit({ legacyHeaders: false, }); -// Pre-check: reject already-registered emails BEFORE rate limiting (BUG-022) function rejectDuplicateEmail(req: Request, res: Response, next: Function) { const { email } = req.body || {}; if (email && typeof email === "string") { @@ -35,7 +34,7 @@ function rejectDuplicateEmail(req: Request, res: Response, next: Function) { next(); } -// Step 1: Request signup — generates 6-digit code +// Step 1: Request signup — generates 6-digit code, sends via email router.post("/free", rejectDuplicateEmail, signupLimiter, async (req: Request, res: Response) => { const { email } = req.body || {}; @@ -53,15 +52,14 @@ router.post("/free", rejectDuplicateEmail, signupLimiter, async (req: Request, r const pending = createPendingVerification(cleanEmail); - // TODO: Send code via email once SMTP is configured - // For now, return code in response for testing - console.log(`šŸ“§ Verification code for ${cleanEmail}: ${pending.code}`); + // Send verification code via email (fire-and-forget, don't block response) + sendVerificationEmail(cleanEmail, pending.code).catch(err => { + console.error(`Failed to send verification email to ${cleanEmail}:`, err); + }); res.json({ status: "verification_required", - message: "Check your email for a verification code", - email: cleanEmail, - code: pending.code, // TEMP: remove once email infra is live + message: "Check your email for the verification code.", }); }); @@ -87,7 +85,6 @@ router.post("/verify", verifyLimiter, (req: Request, res: Response) => { switch (result.status) { case "ok": { const keyInfo = createFreeKey(cleanEmail); - // Mark as verified via legacy system too const verification = createVerification(cleanEmail, keyInfo.key); verification.verifiedAt = new Date().toISOString(); diff --git a/src/services/email.ts b/src/services/email.ts index d169e3c..04372c2 100644 --- a/src/services/email.ts +++ b/src/services/email.ts @@ -1,52 +1,27 @@ -const RESEND_API_KEY = process.env.RESEND_API_KEY; -const BASE_URL = process.env.BASE_URL || "https://docfast.dev"; +import nodemailer from "nodemailer"; -export async function sendVerificationEmail(email: string, token: string): Promise { - const verifyUrl = `${BASE_URL}/verify?token=${token}`; - - if (!RESEND_API_KEY || RESEND_API_KEY === "FILL_IN") { - console.log(`\nšŸ“§ VERIFICATION EMAIL (no Resend configured)`); - console.log(` To: ${email}`); - console.log(` URL: ${verifyUrl}\n`); - return true; - } +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || "host.docker.internal", + port: Number(process.env.SMTP_PORT || 25), + secure: false, + connectionTimeout: 5000, + greetingTimeout: 5000, + socketTimeout: 10000, + tls: { rejectUnauthorized: false }, +}); +export async function sendVerificationEmail(email: string, code: string): Promise { try { - const res = await fetch("https://api.resend.com/emails", { - method: "POST", - headers: { - "Authorization": `Bearer ${RESEND_API_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - from: "DocFast ", - to: [email], - subject: "Verify your DocFast account", - html: ` -
-

⚔ Welcome to DocFast

-

Click the button below to verify your email and get your API key:

- Verify Email → -

This link expires in 24 hours. If you didn't sign up for DocFast, ignore this email.

-
- `, - }), + const info = await transporter.sendMail({ + from: "DocFast ", + to: email, + subject: "DocFast - Verify your email", + text: `Your DocFast verification code is: ${code}\n\nThis code expires in 15 minutes.\n\nIf you didn't sign up for DocFast, ignore this email.`, }); - - if (!res.ok) { - const err = await res.text(); - console.error(`Resend API error: ${res.status} ${err}`); - // Fallback to console - console.log(`\nšŸ“§ VERIFICATION EMAIL (Resend failed, logging)`); - console.log(` To: ${email}`); - console.log(` URL: ${verifyUrl}\n`); - } + console.log(`šŸ“§ Verification email sent to ${email}: ${info.messageId}`); return true; } catch (err) { - console.error("Email send error:", err); - console.log(`\nšŸ“§ VERIFICATION EMAIL (fallback)`); - console.log(` To: ${email}`); - console.log(` URL: ${verifyUrl}\n`); - return true; + console.error(`šŸ“§ Failed to send verification email to ${email}:`, err); + return false; } }