From 2793207b39acfb513312ccad0502cf004da2c0e4 Mon Sep 17 00:00:00 2001 From: Hoid Date: Sun, 8 Mar 2026 08:07:20 +0100 Subject: [PATCH] Remove dead token-based verification system - 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. --- .../dead-token-verification-removal.test.ts | 101 ++++++++++++++++++ src/__tests__/setup.ts | 3 - src/__tests__/signup.test.ts | 4 +- src/index.ts | 59 +--------- src/routes/signup.ts | 4 +- src/services/verification.ts | 83 +------------- 6 files changed, 108 insertions(+), 146 deletions(-) create mode 100644 src/__tests__/dead-token-verification-removal.test.ts diff --git a/src/__tests__/dead-token-verification-removal.test.ts b/src/__tests__/dead-token-verification-removal.test.ts new file mode 100644 index 0000000..dfd0510 --- /dev/null +++ b/src/__tests__/dead-token-verification-removal.test.ts @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 1629eab..19fff85 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -72,12 +72,9 @@ vi.mock("../services/browser.js", () => ({ // Mock verification service 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" }), verifyCode: vi.fn().mockResolvedValue({ status: "ok" }), 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), })); diff --git a/src/__tests__/signup.test.ts b/src/__tests__/signup.test.ts index 5022dd2..1621e05 100644 --- a/src/__tests__/signup.test.ts +++ b/src/__tests__/signup.test.ts @@ -8,7 +8,7 @@ beforeEach(async () => { vi.clearAllMocks(); 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 { 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(verifyCode).mockResolvedValue({ status: "ok" }); 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); const { signupRouter } = await import("../routes/signup.js"); diff --git a/src/index.ts b/src/index.ts index 6255d84..ecd5a6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ import { getUsageStats, getUsageForKey } from "./middleware/usage.js"; import { pdfRateLimitMiddleware, getConcurrencyStats } from "./middleware/pdfRateLimit.js"; import { initBrowser, closeBrowser } from "./services/browser.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 { swaggerSpec } from "./swagger.js"; @@ -215,63 +215,7 @@ app.post("/admin/cleanup", authMiddleware, adminAuth, async (_req: any, res: any }); // 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 ` - -${title} โ€” DocFast - - - -
-

${title}

-

${message}

-${apiKey ? ` -
โš ๏ธ Save your API key securely. You can recover it via email if needed.
-
${apiKey}
- -` : ``} -
- -`; -} // Landing page const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -415,7 +359,6 @@ async function start() { // Load data from PostgreSQL await loadKeys(); - await loadVerifications(); await loadUsageData(); await initBrowser(); diff --git a/src/routes/signup.ts b/src/routes/signup.ts index f786f27..e9f21a1 100644 --- a/src/routes/signup.ts +++ b/src/routes/signup.ts @@ -1,7 +1,7 @@ import { Router, Request, Response } from "express"; import rateLimit from "express-rate-limit"; 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 logger from "../services/logger.js"; @@ -140,8 +140,6 @@ router.post("/verify", verifyLimiter, async (req: Request, res: Response) => { 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", diff --git a/src/services/verification.ts b/src/services/verification.ts index 19df24f..42687b6 100644 --- a/src/services/verification.ts +++ b/src/services/verification.ts @@ -1,15 +1,6 @@ -import { randomBytes, randomInt, timingSafeEqual } from "crypto"; +import { randomInt, timingSafeEqual } from "crypto"; import logger from "./logger.js"; -import pool from "./db.js"; -import { queryWithRetry, connectWithRetry } from "./db.js"; - -export interface Verification { - email: string; - token: string; - apiKey: string; - createdAt: string; - verifiedAt: string | null; -} +import { queryWithRetry } from "./db.js"; export interface PendingVerification { email: string; @@ -19,77 +10,9 @@ export interface PendingVerification { attempts: number; } -const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; const CODE_EXPIRY_MS = 15 * 60 * 1000; const MAX_ATTEMPTS = 3; -export async function createVerification(email: string, apiKey: string): Promise { - // 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 { - 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 { await queryWithRetry("DELETE FROM pending_verifications WHERE email = $1", [email]); @@ -151,4 +74,4 @@ export async function getVerifiedApiKey(email: string): Promise { [email] ); return result.rows[0]?.api_key ?? null; -} +} \ No newline at end of file