security: remove Change Email feature (leaked key = account hijack)
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 1m7s
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 1m7s
This commit is contained in:
parent
1702abdeb8
commit
f5cea97adf
12 changed files with 80 additions and 849 deletions
|
|
@ -12,7 +12,6 @@ import { healthRouter } from "./routes/health.js";
|
|||
import { signupRouter } from "./routes/signup.js";
|
||||
import { recoverRouter } from "./routes/recover.js";
|
||||
import { billingRouter } from "./routes/billing.js";
|
||||
import { emailChangeRouter } from "./routes/email-change.js";
|
||||
import { authMiddleware } from "./middleware/auth.js";
|
||||
import { usageMiddleware, loadUsageData } from "./middleware/usage.js";
|
||||
import { getUsageStats } from "./middleware/usage.js";
|
||||
|
|
@ -56,7 +55,6 @@ app.use((req, res, next) => {
|
|||
const isAuthBillingRoute = req.path.startsWith('/v1/signup') ||
|
||||
req.path.startsWith('/v1/recover') ||
|
||||
req.path.startsWith('/v1/billing') ||
|
||||
req.path.startsWith('/v1/email-change');
|
||||
|
||||
if (isAuthBillingRoute) {
|
||||
res.setHeader("Access-Control-Allow-Origin", "https://docfast.dev");
|
||||
|
|
@ -97,7 +95,6 @@ app.use("/health", healthRouter);
|
|||
app.use("/v1/signup", signupRouter);
|
||||
app.use("/v1/recover", recoverRouter);
|
||||
app.use("/v1/billing", billingRouter);
|
||||
app.use("/v1/email-change", emailChangeRouter);
|
||||
|
||||
// Authenticated routes — conversion routes get tighter body limits (500KB)
|
||||
const convertBodyLimit = express.json({ limit: "500kb" });
|
||||
|
|
@ -217,11 +214,6 @@ app.get("/terms", (_req, res) => {
|
|||
res.sendFile(path.join(__dirname, "../public/terms.html"));
|
||||
});
|
||||
|
||||
app.get("/change-email", (_req, res) => {
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
res.sendFile(path.join(__dirname, "../public/change-email.html"));
|
||||
});
|
||||
|
||||
app.get("/status", (_req, res) => {
|
||||
res.setHeader("Cache-Control", "public, max-age=60");
|
||||
res.sendFile(path.join(__dirname, "../public/status.html"));
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
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";
|
||||
import logger from "../services/logger.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,
|
||||
});
|
||||
|
||||
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 = await createPendingVerification(cleanEmail);
|
||||
|
||||
sendVerificationEmail(cleanEmail, (pending as any).code).catch((err: Error) => {
|
||||
logger.error({ err, email: cleanEmail }, "Failed to send email change verification");
|
||||
});
|
||||
|
||||
res.json({ status: "verification_sent", message: "Verification code sent to your new email address." });
|
||||
});
|
||||
|
||||
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 = await verifyCode(cleanEmail, cleanCode);
|
||||
|
||||
switch (result.status) {
|
||||
case "ok": {
|
||||
const updated = await 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 };
|
||||
Loading…
Add table
Add a link
Reference in a new issue