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

View file

@ -1,13 +1,15 @@
import { Router, Request, Response } from "express";
import rateLimit from "express-rate-limit";
import { createFreeKey } from "../services/keys.js";
import { createVerification, verifyToken, isEmailVerified, getVerifiedApiKey } from "../services/verification.js";
import { sendVerificationEmail } from "../services/email.js";
const router = Router();
// Rate limiting for signup - 5 signups per IP per hour
const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 signups per IP per hour
windowMs: 60 * 60 * 1000,
max: 5,
message: {
error: "Too many signup attempts. Please try again in 1 hour.",
retryAfter: "1 hour"
@ -18,29 +20,36 @@ const signupLimiter = rateLimit({
skipFailedRequests: false,
});
// Self-service free API key signup
router.post("/free", signupLimiter, (req: Request, res: Response) => {
// Self-service free API key signup — now requires email verification
router.post("/free", signupLimiter, async (req: Request, res: Response) => {
const { email } = req.body || {};
// Email is optional — validate only if provided
let cleanEmail: string | undefined;
if (email && typeof email === "string") {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
res.status(400).json({ error: "Invalid email address" });
return;
}
cleanEmail = email.trim().toLowerCase();
if (!email || typeof email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
res.status(400).json({ error: "A valid email address is required." });
return;
}
const cleanEmail = email.trim().toLowerCase();
// If already verified, tell them
if (isEmailVerified(cleanEmail)) {
res.status(409).json({ error: "This email is already registered. Check your inbox for the original verification email, or contact support." });
return;
}
// Create the API key (but don't reveal it yet)
const keyInfo = createFreeKey(cleanEmail);
// Create verification record
const verification = createVerification(cleanEmail, keyInfo.key);
// Send verification email
await sendVerificationEmail(cleanEmail, verification.token);
res.json({
message: "Welcome to DocFast! 🚀",
apiKey: keyInfo.key,
tier: "free",
limit: "100 PDFs/month",
docs: "https://docfast.dev/docs",
note: "Save this API key — it won't be shown again.",
message: "Check your email for a verification link to get your API key.",
email: cleanEmail,
verified: false,
});
});