docfast/src/routes/email-change.ts
OpenClaw 6aa1fa4d84 fix: swagger UI symlink, CSP headers, email-change route, updateKeyEmail
- Fix swagger-ui symlink in Dockerfile (was pointing to /opt/docfast instead of /app)
- Add CSP directives to allow inline scripts/styles and Google Fonts
- Add email-change.ts route with rate limiting (3/hr) and verification
- Add updateKeyEmail to keys service
- Add email-change route to index.ts with CORS support
2026-02-14 22:29:56 +00:00

101 lines
3.5 KiB
TypeScript

import { Router } from "express";
import type { Request, Response } from "express";
import rateLimit from "express-rate-limit";
import { createPendingVerification, verifyCode } from "../services/verification.js";
import { sendVerificationEmail } from "../services/email.js";
import { getAllKeys, updateKeyEmail } from "../services/keys.js";
const router = Router();
const changeLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 3,
message: { error: "Too many attempts. Please try again in 1 hour." },
standardHeaders: true,
legacyHeaders: false,
});
// Step 1: Request email change — sends verification code to NEW email
router.post("/", changeLimiter, async (req: Request, res: Response) => {
const apiKey = req.headers.authorization?.replace(/^Bearer\s+/i, "") || req.body?.apiKey;
const newEmail = req.body?.newEmail;
if (!apiKey || typeof apiKey !== "string") {
res.status(400).json({ error: "API key is required (Authorization header or body)." });
return;
}
if (!newEmail || typeof newEmail !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
res.status(400).json({ error: "A valid new email address is required." });
return;
}
const cleanEmail = newEmail.trim().toLowerCase();
const keys = getAllKeys();
const userKey = keys.find((k: any) => k.key === apiKey);
if (!userKey) {
res.status(401).json({ error: "Invalid API key." });
return;
}
const existing = keys.find((k: any) => k.email === cleanEmail);
if (existing) {
res.status(409).json({ error: "This email is already associated with another account." });
return;
}
const pending = createPendingVerification(cleanEmail);
(pending as any)._changeContext = { apiKey, newEmail: cleanEmail, oldEmail: userKey.email };
sendVerificationEmail(cleanEmail, (pending as any).code).catch((err: Error) => {
console.error(`Failed to send email change verification to ${cleanEmail}:`, err);
});
res.json({ status: "verification_sent", message: "Verification code sent to your new email address." });
});
// Step 2: Verify code — updates email
router.post("/verify", changeLimiter, async (req: Request, res: Response) => {
const apiKey = req.headers.authorization?.replace(/^Bearer\s+/i, "") || req.body?.apiKey;
const { newEmail, code } = req.body || {};
if (!apiKey || !newEmail || !code) {
res.status(400).json({ error: "API key, new email, and code are required." });
return;
}
const cleanEmail = newEmail.trim().toLowerCase();
const cleanCode = String(code).trim();
const keys = getAllKeys();
const userKey = keys.find((k: any) => k.key === apiKey);
if (!userKey) {
res.status(401).json({ error: "Invalid API key." });
return;
}
const result = verifyCode(cleanEmail, cleanCode);
switch (result.status) {
case "ok": {
const updated = updateKeyEmail(apiKey, cleanEmail);
if (updated) {
res.json({ status: "updated", message: "Email address updated successfully.", newEmail: cleanEmail });
} else {
res.status(500).json({ error: "Failed to update email." });
}
break;
}
case "expired":
res.status(410).json({ error: "Verification code has expired. Please request a new one." });
break;
case "max_attempts":
res.status(429).json({ error: "Too many failed attempts. Please request a new code." });
break;
case "invalid":
res.status(400).json({ error: "Invalid verification code." });
break;
}
});
export { router as emailChangeRouter };