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
This commit is contained in:
OpenClaw 2026-02-14 19:10:45 +00:00
parent a67c16cd0f
commit 210fb26ec1
5 changed files with 50 additions and 54 deletions

View file

@ -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();

View file

@ -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<boolean> {
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<boolean> {
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 <noreply@docfast.dev>",
to: [email],
subject: "Verify your DocFast account",
html: `
<div style="font-family: -apple-system, sans-serif; max-width: 480px; margin: 0 auto; padding: 40px 20px;">
<h1 style="font-size: 24px; margin-bottom: 16px;"> Welcome to DocFast</h1>
<p style="color: #555; line-height: 1.6;">Click the button below to verify your email and get your API key:</p>
<a href="${verifyUrl}" style="display: inline-block; background: #34d399; color: #0b0d11; padding: 14px 28px; border-radius: 8px; font-weight: 600; text-decoration: none; margin: 24px 0;">Verify Email </a>
<p style="color: #999; font-size: 13px; margin-top: 24px;">This link expires in 24 hours. If you didn't sign up for DocFast, ignore this email.</p>
</div>
`,
}),
const info = await transporter.sendMail({
from: "DocFast <noreply@docfast.dev>",
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;
}
}