Remove dead token-based verification system
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m26s
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:
parent
d376d586fe
commit
2793207b39
6 changed files with 108 additions and 146 deletions
101
src/__tests__/dead-token-verification-removal.test.ts
Normal file
101
src/__tests__/dead-token-verification-removal.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
59
src/index.ts
59
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 `<!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
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<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> {
|
||||
await queryWithRetry("DELETE FROM pending_verifications WHERE email = $1", [email]);
|
||||
|
||||
|
|
@ -151,4 +74,4 @@ export async function getVerifiedApiKey(email: string): Promise<string | null> {
|
|||
[email]
|
||||
);
|
||||
return result.rows[0]?.api_key ?? null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue