feat: email verification for free tier signup

- Signup now requires email verification before API key is revealed
- Verification token sent via email (Resend) with console fallback
- GET /verify?token=xxx shows API key in styled HTML page
- Handles expired (24h), invalid, and already-verified tokens
- Frontend modal shows 'check your email' instead of key
- Keeps existing rate limiting
This commit is contained in:
OpenClaw 2026-02-14 18:12:25 +00:00
parent 890b82e5ec
commit 1b20665b0d
7 changed files with 252 additions and 29 deletions

52
src/services/email.ts Normal file
View file

@ -0,0 +1,52 @@
const RESEND_API_KEY = process.env.RESEND_API_KEY;
const BASE_URL = process.env.BASE_URL || "https://docfast.dev";
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;
}
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>
`,
}),
});
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`);
}
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;
}
}