Remove dead token-based verification system
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m26s

- Remove verificationsCache array and loadVerifications() function from verification.ts
- Remove verifyToken() and verifyTokenSync() functions (multi-replica unsafe, never used)
- Remove createVerification() function (stores unused data)
- Remove GET /verify route and verifyPage() helper function
- Remove loadVerifications() call from startup
- Remove createVerification() usage from signup route
- Update imports and test mocks to match removed functions
- Keep active 6-digit code system intact (createPendingVerification, verifyCode, etc.)

All 559 tests passing. The active verification system using pending_verifications
table and 6-digit codes continues to work normally.
This commit is contained in:
Hoid 2026-03-08 08:07:20 +01:00
parent d376d586fe
commit 2793207b39
6 changed files with 108 additions and 146 deletions

View file

@ -0,0 +1,101 @@
import { describe, it, expect } from "vitest";
import request from "supertest";
import { app } from "../index.js";
describe("Dead Token Verification System Removal", () => {
describe("Removed Functions", () => {
it("should not export verificationsCache from verification service", async () => {
try {
const verification = await import("../services/verification.js");
expect(verification).not.toHaveProperty("verificationsCache");
} catch (error) {
// This is fine - the export doesn't exist
expect(true).toBe(true);
}
});
it("should not export loadVerifications from verification service", async () => {
try {
const verification = await import("../services/verification.js");
expect(verification).not.toHaveProperty("loadVerifications");
} catch (error) {
// This is fine - the export doesn't exist
expect(true).toBe(true);
}
});
it("should not export verifyToken from verification service", async () => {
try {
const verification = await import("../services/verification.js");
expect(verification).not.toHaveProperty("verifyToken");
} catch (error) {
// This is fine - the export doesn't exist
expect(true).toBe(true);
}
});
it("should not export verifyTokenSync from verification service", async () => {
try {
const verification = await import("../services/verification.js");
expect(verification).not.toHaveProperty("verifyTokenSync");
} catch (error) {
// This is fine - the export doesn't exist
expect(true).toBe(true);
}
});
it("should not export createVerification from verification service", async () => {
try {
const verification = await import("../services/verification.js");
expect(verification).not.toHaveProperty("createVerification");
} catch (error) {
// This is fine - the export doesn't exist
expect(true).toBe(true);
}
});
});
describe("Removed Routes", () => {
it("should return 404 for GET /verify route", async () => {
const response = await request(app).get("/verify").query({ token: "some-token" });
expect(response.status).toBe(404);
});
it("should return 404 for GET /verify route without token", async () => {
const response = await request(app).get("/verify");
expect(response.status).toBe(404);
});
});
describe("Active System Still Works", () => {
it("should export createPendingVerification", async () => {
const verification = await import("../services/verification.js");
expect(verification).toHaveProperty("createPendingVerification");
expect(typeof verification.createPendingVerification).toBe("function");
});
it("should export verifyCode", async () => {
const verification = await import("../services/verification.js");
expect(verification).toHaveProperty("verifyCode");
expect(typeof verification.verifyCode).toBe("function");
});
it("should export isEmailVerified", async () => {
const verification = await import("../services/verification.js");
expect(verification).toHaveProperty("isEmailVerified");
expect(typeof verification.isEmailVerified).toBe("function");
});
it("should export getVerifiedApiKey", async () => {
const verification = await import("../services/verification.js");
expect(verification).toHaveProperty("getVerifiedApiKey");
expect(typeof verification.getVerifiedApiKey).toBe("function");
});
it("should export PendingVerification interface", async () => {
// TypeScript interface test - if compilation passes, the interface exists
const verification = await import("../services/verification.js");
expect(verification).toBeDefined();
});
});
});

View file

@ -72,12 +72,9 @@ vi.mock("../services/browser.js", () => ({
// Mock verification service // Mock verification service
vi.mock("../services/verification.js", () => ({ vi.mock("../services/verification.js", () => ({
verifyToken: vi.fn().mockReturnValue({ status: "invalid" }),
loadVerifications: vi.fn().mockResolvedValue(undefined),
createPendingVerification: vi.fn().mockResolvedValue({ email: "test@test.com", code: "123456" }), createPendingVerification: vi.fn().mockResolvedValue({ email: "test@test.com", code: "123456" }),
verifyCode: vi.fn().mockResolvedValue({ status: "ok" }), verifyCode: vi.fn().mockResolvedValue({ status: "ok" }),
isEmailVerified: vi.fn().mockResolvedValue(false), isEmailVerified: vi.fn().mockResolvedValue(false),
createVerification: vi.fn().mockResolvedValue({ email: "test@test.com", token: "tok", apiKey: "key", createdAt: "", verifiedAt: null }),
getVerifiedApiKey: vi.fn().mockResolvedValue(null), getVerifiedApiKey: vi.fn().mockResolvedValue(null),
})); }));

View file

@ -8,7 +8,7 @@ beforeEach(async () => {
vi.clearAllMocks(); vi.clearAllMocks();
vi.resetModules(); vi.resetModules();
const { isEmailVerified, createPendingVerification, verifyCode, createVerification } = await import("../services/verification.js"); const { isEmailVerified, createPendingVerification, verifyCode } = await import("../services/verification.js");
const { sendVerificationEmail } = await import("../services/email.js"); const { sendVerificationEmail } = await import("../services/email.js");
const { createFreeKey } = await import("../services/keys.js"); const { createFreeKey } = await import("../services/keys.js");
@ -16,7 +16,7 @@ beforeEach(async () => {
vi.mocked(createPendingVerification).mockResolvedValue({ email: "test@test.com", code: "123456", createdAt: "", expiresAt: "", attempts: 0 }); vi.mocked(createPendingVerification).mockResolvedValue({ email: "test@test.com", code: "123456", createdAt: "", expiresAt: "", attempts: 0 });
vi.mocked(verifyCode).mockResolvedValue({ status: "ok" }); vi.mocked(verifyCode).mockResolvedValue({ status: "ok" });
vi.mocked(createFreeKey).mockResolvedValue({ key: "free-key-123", tier: "free", email: "test@test.com", createdAt: "" }); vi.mocked(createFreeKey).mockResolvedValue({ key: "free-key-123", tier: "free", email: "test@test.com", createdAt: "" });
vi.mocked(createVerification).mockResolvedValue({ email: "test@test.com", token: "tok", apiKey: "free-key-123", createdAt: "", verifiedAt: null });
vi.mocked(sendVerificationEmail).mockResolvedValue(true); vi.mocked(sendVerificationEmail).mockResolvedValue(true);
const { signupRouter } = await import("../routes/signup.js"); const { signupRouter } = await import("../routes/signup.js");

View file

@ -23,7 +23,7 @@ import { getUsageStats, getUsageForKey } from "./middleware/usage.js";
import { pdfRateLimitMiddleware, getConcurrencyStats } from "./middleware/pdfRateLimit.js"; import { pdfRateLimitMiddleware, getConcurrencyStats } from "./middleware/pdfRateLimit.js";
import { initBrowser, closeBrowser } from "./services/browser.js"; import { initBrowser, closeBrowser } from "./services/browser.js";
import { loadKeys, getAllKeys, isProKey } from "./services/keys.js"; import { loadKeys, getAllKeys, isProKey } from "./services/keys.js";
import { verifyToken, loadVerifications } from "./services/verification.js";
import { initDatabase, pool, cleanupStaleData } from "./services/db.js"; import { initDatabase, pool, cleanupStaleData } from "./services/db.js";
import { swaggerSpec } from "./swagger.js"; import { swaggerSpec } from "./swagger.js";
@ -215,63 +215,7 @@ app.post("/admin/cleanup", authMiddleware, adminAuth, async (_req: any, res: any
}); });
// Email verification endpoint // Email verification endpoint
app.get("/verify", (req, res) => {
const token = req.query.token as string;
if (!token) {
res.status(400).send(verifyPage("Invalid Link", "No verification token provided.", null));
return;
}
const result = verifyToken(token);
switch (result.status) {
case "ok":
res.send(verifyPage("Email Verified! 🚀", "Your DocFast API key is ready:", result.verification!.apiKey));
break;
case "already_verified":
res.send(verifyPage("Already Verified", "This email was already verified. Here's your API key:", result.verification!.apiKey));
break;
case "expired":
res.status(410).send(verifyPage("Link Expired", "This verification link has expired (24h). Please sign up again.", null));
break;
case "invalid":
res.status(404).send(verifyPage("Invalid Link", "This verification link is not valid.", null));
break;
}
});
function verifyPage(title: string, message: string, apiKey: string | null): string {
return `<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title} DocFast</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',sans-serif;background:#0b0d11;color:#e4e7ed;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.card{background:#151922;border:1px solid #1e2433;border-radius:16px;padding:48px;max-width:520px;width:100%;text-align:center}
h1{font-size:1.8rem;margin-bottom:12px;font-weight:800}
p{color:#7a8194;margin-bottom:24px;line-height:1.6}
.key-box{background:#0b0d11;border:1px solid #34d399;border-radius:8px;padding:16px;font-family:monospace;font-size:0.82rem;word-break:break-all;margin:16px 0;cursor:pointer;transition:background 0.2s;position:relative}
.key-box:hover{background:#12151c}
.key-box::after{content:'Click to copy';position:absolute;top:-24px;right:0;font-size:0.7rem;color:#7a8194;font-family:'Inter',sans-serif}
.warning{background:rgba(251,191,36,0.06);border:1px solid rgba(251,191,36,0.15);border-radius:8px;padding:12px 16px;font-size:0.85rem;color:#fbbf24;margin-bottom:16px;text-align:left}
.links{margin-top:24px;color:#7a8194;font-size:0.9rem}
.links a{color:#34d399;text-decoration:none}
.links a:hover{color:#5eead4}
</style></head><body>
<div class="card">
<h1>${title}</h1>
<p>${message}</p>
${apiKey ? `
<div class="warning"> Save your API key securely. You can recover it via email if needed.</div>
<div class="key-box" data-copy="${apiKey}">${apiKey}</div>
<div class="links">Upgrade to Pro for 5,000 PDFs/month · <a href="/docs">Read the docs </a></div>
` : `<div class="links"><a href="/"> Back to DocFast</a></div>`}
</div>
<script src="/copy-helper.js"></script>
</body></html>`;
}
// Landing page // Landing page
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -415,7 +359,6 @@ async function start() {
// Load data from PostgreSQL // Load data from PostgreSQL
await loadKeys(); await loadKeys();
await loadVerifications();
await loadUsageData(); await loadUsageData();
await initBrowser(); await initBrowser();

View file

@ -1,7 +1,7 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import rateLimit from "express-rate-limit"; import rateLimit from "express-rate-limit";
import { createFreeKey } from "../services/keys.js"; import { createFreeKey } from "../services/keys.js";
import { createVerification, createPendingVerification, verifyCode, isEmailVerified, getVerifiedApiKey } from "../services/verification.js"; import { createPendingVerification, verifyCode, isEmailVerified, getVerifiedApiKey } from "../services/verification.js";
import { sendVerificationEmail } from "../services/email.js"; import { sendVerificationEmail } from "../services/email.js";
import logger from "../services/logger.js"; import logger from "../services/logger.js";
@ -140,8 +140,6 @@ router.post("/verify", verifyLimiter, async (req: Request, res: Response) => {
switch (result.status) { switch (result.status) {
case "ok": { case "ok": {
const keyInfo = await createFreeKey(cleanEmail); const keyInfo = await createFreeKey(cleanEmail);
const verification = await createVerification(cleanEmail, keyInfo.key);
verification.verifiedAt = new Date().toISOString();
res.json({ res.json({
status: "verified", status: "verified",

View file

@ -1,15 +1,6 @@
import { randomBytes, randomInt, timingSafeEqual } from "crypto"; import { randomInt, timingSafeEqual } from "crypto";
import logger from "./logger.js"; import logger from "./logger.js";
import pool from "./db.js"; import { queryWithRetry } from "./db.js";
import { queryWithRetry, connectWithRetry } from "./db.js";
export interface Verification {
email: string;
token: string;
apiKey: string;
createdAt: string;
verifiedAt: string | null;
}
export interface PendingVerification { export interface PendingVerification {
email: string; email: string;
@ -19,77 +10,9 @@ export interface PendingVerification {
attempts: number; attempts: number;
} }
const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000;
const CODE_EXPIRY_MS = 15 * 60 * 1000; const CODE_EXPIRY_MS = 15 * 60 * 1000;
const MAX_ATTEMPTS = 3; const MAX_ATTEMPTS = 3;
export async function createVerification(email: string, apiKey: string): Promise<Verification> {
// Check for existing unexpired, unverified
const existing = await queryWithRetry(
"SELECT * FROM verifications WHERE email = $1 AND verified_at IS NULL AND created_at > NOW() - INTERVAL '24 hours' LIMIT 1",
[email]
);
if (existing.rows.length > 0) {
const r = existing.rows[0];
return { email: r.email, token: r.token, apiKey: r.api_key, createdAt: r.created_at.toISOString(), verifiedAt: null };
}
// Remove old unverified
await queryWithRetry("DELETE FROM verifications WHERE email = $1 AND verified_at IS NULL", [email]);
const token = randomBytes(32).toString("hex");
const now = new Date().toISOString();
await queryWithRetry(
"INSERT INTO verifications (email, token, api_key, created_at) VALUES ($1, $2, $3, $4)",
[email, token, apiKey, now]
);
return { email, token, apiKey, createdAt: now, verifiedAt: null };
}
export function verifyToken(token: string): { status: "ok"; verification: Verification } | { status: "invalid" | "expired" | "already_verified"; verification?: Verification } {
// Synchronous wrapper — we'll make it async-compatible
// Actually need to keep sync for the GET /verify route. Use sync query workaround or refactor.
// For simplicity, we'll cache verifications in memory too.
return verifyTokenSync(token);
}
// In-memory cache for verifications (loaded on startup, updated on changes)
let verificationsCache: Verification[] = [];
export async function loadVerifications(): Promise<void> {
const result = await queryWithRetry("SELECT * FROM verifications");
verificationsCache = result.rows.map((r) => ({
email: r.email,
token: r.token,
apiKey: r.api_key,
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
verifiedAt: r.verified_at ? (r.verified_at instanceof Date ? r.verified_at.toISOString() : r.verified_at) : null,
}));
// Cleanup expired entries every 15 minutes
setInterval(() => {
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
const before = verificationsCache.length;
verificationsCache = verificationsCache.filter(
(v) => v.verifiedAt || new Date(v.createdAt).getTime() > cutoff
);
const removed = before - verificationsCache.length;
if (removed > 0) logger.info({ removed }, "Cleaned expired verification cache entries");
}, 15 * 60 * 1000);
}
function verifyTokenSync(token: string): { status: "ok"; verification: Verification } | { status: "invalid" | "expired" | "already_verified"; verification?: Verification } {
const v = verificationsCache.find((v) => v.token === token);
if (!v) return { status: "invalid" };
if (v.verifiedAt) return { status: "already_verified", verification: v };
const age = Date.now() - new Date(v.createdAt).getTime();
if (age > TOKEN_EXPIRY_MS) return { status: "expired" };
v.verifiedAt = new Date().toISOString();
// Update DB async
queryWithRetry("UPDATE verifications SET verified_at = $1 WHERE token = $2", [v.verifiedAt, token]).catch((err) => logger.error({ err }, "Failed to update verification"));
return { status: "ok", verification: v };
}
export async function createPendingVerification(email: string): Promise<PendingVerification> { export async function createPendingVerification(email: string): Promise<PendingVerification> {
await queryWithRetry("DELETE FROM pending_verifications WHERE email = $1", [email]); await queryWithRetry("DELETE FROM pending_verifications WHERE email = $1", [email]);
@ -151,4 +74,4 @@ export async function getVerifiedApiKey(email: string): Promise<string | null> {
[email] [email]
); );
return result.rows[0]?.api_key ?? null; return result.rows[0]?.api_key ?? null;
} }