import { Router } from "express"; import rateLimit from "express-rate-limit"; import { createFreeKey } from "../services/keys.js"; import { createVerification, createPendingVerification, verifyCode, isEmailVerified } from "../services/verification.js"; import { sendVerificationEmail } from "../services/email.js"; import logger from "../services/logger.js"; const router = Router(); const signupLimiter = rateLimit({ windowMs: 60 * 60 * 1000, max: 5, message: { error: "Too many signup attempts. Please try again in 1 hour." }, standardHeaders: true, legacyHeaders: false, }); const verifyLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 15, message: { error: "Too many verification attempts. Please try again later." }, standardHeaders: true, legacyHeaders: false, }); async function rejectDuplicateEmail(req, res, next) { const { email } = req.body || {}; if (email && typeof email === "string") { const cleanEmail = email.trim().toLowerCase(); if (await isEmailVerified(cleanEmail)) { res.status(409).json({ error: "Email already registered" }); return; } } next(); } // Step 1: Request signup — generates 6-digit code, sends via email router.post("/free", rejectDuplicateEmail, signupLimiter, async (req, res) => { const { email } = req.body || {}; 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 (await isEmailVerified(cleanEmail)) { res.status(409).json({ error: "This email is already registered. Contact support if you need help." }); return; } const pending = await createPendingVerification(cleanEmail); sendVerificationEmail(cleanEmail, pending.code).catch(err => { logger.error({ err, email: cleanEmail }, "Failed to send verification email"); }); res.json({ status: "verification_required", message: "Check your email for the verification code.", }); }); /** * @openapi * /v1/signup/verify: * post: * tags: [Account] * summary: Verify email and get API key (discontinued) * deprecated: true * description: | * **Discontinued.** Free accounts are no longer available. Try the demo at POST /v1/demo/html or upgrade to Pro at https://docfast.dev. * Rate limited to 15 attempts per 15 minutes. * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [email, code] * properties: * email: * type: string * format: email * description: Email address used during signup * example: user@example.com * code: * type: string * description: 6-digit verification code from email * example: "123456" * responses: * 200: * description: Email verified, API key issued * content: * application/json: * schema: * type: object * properties: * status: * type: string * example: verified * message: * type: string * apiKey: * type: string * description: The provisioned API key * tier: * type: string * example: free * 400: * description: Missing fields or invalid verification code * 409: * description: Email already verified * 410: * description: Verification code expired * 429: * description: Too many failed attempts */ // Step 2: Verify code — creates API key router.post("/verify", verifyLimiter, async (req, res) => { const { email, code } = req.body || {}; if (!email || !code) { res.status(400).json({ error: "Email and code are required." }); return; } const cleanEmail = email.trim().toLowerCase(); const cleanCode = String(code).trim(); if (await isEmailVerified(cleanEmail)) { res.status(409).json({ error: "This email is already verified." }); return; } const result = await verifyCode(cleanEmail, cleanCode); switch (result.status) { case "ok": { const keyInfo = await createFreeKey(cleanEmail); const verification = await createVerification(cleanEmail, keyInfo.key); verification.verifiedAt = new Date().toISOString(); res.json({ status: "verified", message: "Email verified! Here's your API key.", apiKey: keyInfo.key, tier: keyInfo.tier, }); break; } case "expired": res.status(410).json({ error: "Verification code has expired. Please sign up again." }); break; case "max_attempts": res.status(429).json({ error: "Too many failed attempts. Please sign up again to get a new code." }); break; case "invalid": res.status(400).json({ error: "Invalid verification code." }); break; } }); export { router as signupRouter };