Remove dead signup router, unused verification functions, and legacy cleanup query
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m37s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m37s
- Delete src/routes/signup.ts (dead code, 410 handler in index.ts remains) - Remove isEmailVerified() and getVerifiedApiKey() from verification.ts (only used by signup) - Remove stale-key cleanup from cleanupStaleData() that queried legacy verifications table - Update usage middleware message: 'Free tier limit' → 'Account limit' - TDD: 8 new tests, removed signup.test.ts (dead), net 556 tests passing
This commit is contained in:
parent
921562750f
commit
7206cb518d
11 changed files with 79 additions and 308 deletions
16
src/__tests__/cleanup-no-verifications-table.test.ts
Normal file
16
src/__tests__/cleanup-no-verifications-table.test.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
describe("cleanupStaleData should not reference legacy verifications table", () => {
|
||||||
|
it("should not query verifications table (legacy, no longer written to)", () => {
|
||||||
|
const dbSrc = readFileSync(join(__dirname, "../services/db.ts"), "utf8");
|
||||||
|
// Extract just the cleanupStaleData function body
|
||||||
|
const funcStart = dbSrc.indexOf("async function cleanupStaleData");
|
||||||
|
const funcEnd = dbSrc.indexOf("export { pool }");
|
||||||
|
const funcBody = dbSrc.slice(funcStart, funcEnd);
|
||||||
|
// Should not reference 'verifications' table (only pending_verifications is active)
|
||||||
|
// The old query checked: email NOT IN (SELECT ... FROM verifications WHERE verified_at IS NOT NULL)
|
||||||
|
expect(funcBody).not.toContain("FROM verifications WHERE verified_at");
|
||||||
|
});
|
||||||
|
});
|
||||||
44
src/__tests__/dead-signup-removal.test.ts
Normal file
44
src/__tests__/dead-signup-removal.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { readFileSync, existsSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
describe("Dead Signup Router Removal", () => {
|
||||||
|
describe("Signup router module removed", () => {
|
||||||
|
it("should not have src/routes/signup.ts file", () => {
|
||||||
|
const signupPath = join(__dirname, "../routes/signup.ts");
|
||||||
|
expect(existsSync(signupPath)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Dead verification functions removed from source", () => {
|
||||||
|
it("should not export isEmailVerified from verification.ts source", () => {
|
||||||
|
const src = readFileSync(join(__dirname, "../services/verification.ts"), "utf8");
|
||||||
|
expect(src).not.toContain("export async function isEmailVerified");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not export getVerifiedApiKey from verification.ts source", () => {
|
||||||
|
const src = readFileSync(join(__dirname, "../services/verification.ts"), "utf8");
|
||||||
|
expect(src).not.toContain("export async function getVerifiedApiKey");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still export createPendingVerification", () => {
|
||||||
|
const src = readFileSync(join(__dirname, "../services/verification.ts"), "utf8");
|
||||||
|
expect(src).toContain("export async function createPendingVerification");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still export verifyCode", () => {
|
||||||
|
const src = readFileSync(join(__dirname, "../services/verification.ts"), "utf8");
|
||||||
|
expect(src).toContain("export async function verifyCode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("410 signup handler still works", () => {
|
||||||
|
it("should still have signup 410 handler working", async () => {
|
||||||
|
const request = (await import("supertest")).default;
|
||||||
|
const { app } = await import("../index.js");
|
||||||
|
const res = await request(app).post("/v1/signup/free");
|
||||||
|
expect(res.status).toBe(410);
|
||||||
|
expect(res.body.error).toContain("discontinued");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -80,17 +80,7 @@ describe("Dead Token Verification System Removal", () => {
|
||||||
expect(typeof verification.verifyCode).toBe("function");
|
expect(typeof verification.verifyCode).toBe("function");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should export isEmailVerified", async () => {
|
// isEmailVerified and getVerifiedApiKey removed — only used by dead signup router
|
||||||
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 () => {
|
it("should export PendingVerification interface", async () => {
|
||||||
// TypeScript interface test - if compilation passes, the interface exists
|
// TypeScript interface test - if compilation passes, the interface exists
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,7 @@ vi.mock("../services/browser.js", () => ({
|
||||||
vi.mock("../services/verification.js", () => ({
|
vi.mock("../services/verification.js", () => ({
|
||||||
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),
|
|
||||||
getVerifiedApiKey: vi.fn().mockResolvedValue(null),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock email service
|
// Mock email service
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
import express from "express";
|
|
||||||
import request from "supertest";
|
|
||||||
|
|
||||||
let app: express.Express;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const { isEmailVerified, createPendingVerification, verifyCode } = await import("../services/verification.js");
|
|
||||||
const { sendVerificationEmail } = await import("../services/email.js");
|
|
||||||
const { createFreeKey } = await import("../services/keys.js");
|
|
||||||
|
|
||||||
vi.mocked(isEmailVerified).mockResolvedValue(false);
|
|
||||||
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(sendVerificationEmail).mockResolvedValue(true);
|
|
||||||
|
|
||||||
const { signupRouter } = await import("../routes/signup.js");
|
|
||||||
app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
app.use("/signup", signupRouter);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("POST /signup/free", () => {
|
|
||||||
it("returns 400 for missing email", async () => {
|
|
||||||
const res = await request(app).post("/signup/free").send({});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 400 for invalid email format", async () => {
|
|
||||||
const res = await request(app).post("/signup/free").send({ email: "not-email" });
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 409 for already verified email", async () => {
|
|
||||||
const { isEmailVerified } = await import("../services/verification.js");
|
|
||||||
vi.mocked(isEmailVerified).mockResolvedValue(true);
|
|
||||||
const res = await request(app).post("/signup/free").send({ email: "dup@test.com" });
|
|
||||||
expect(res.status).toBe(409);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 200 with verification_required for valid email", async () => {
|
|
||||||
const res = await request(app).post("/signup/free").send({ email: "new@test.com" });
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(res.body.status).toBe("verification_required");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends verification email asynchronously", async () => {
|
|
||||||
const { sendVerificationEmail } = await import("../services/email.js");
|
|
||||||
await request(app).post("/signup/free").send({ email: "new@test.com" });
|
|
||||||
await new Promise(r => setTimeout(r, 50));
|
|
||||||
expect(sendVerificationEmail).toHaveBeenCalledWith("new@test.com", "123456");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("POST /signup/verify", () => {
|
|
||||||
it("returns 400 for missing email/code", async () => {
|
|
||||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com" });
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 409 for already verified email", async () => {
|
|
||||||
const { isEmailVerified } = await import("../services/verification.js");
|
|
||||||
vi.mocked(isEmailVerified).mockResolvedValue(true);
|
|
||||||
const res = await request(app).post("/signup/verify").send({ email: "dup@test.com", code: "123456" });
|
|
||||||
expect(res.status).toBe(409);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 410 for expired code", async () => {
|
|
||||||
const { verifyCode } = await import("../services/verification.js");
|
|
||||||
vi.mocked(verifyCode).mockResolvedValue({ status: "expired" });
|
|
||||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "123456" });
|
|
||||||
expect(res.status).toBe(410);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 429 for max attempts", async () => {
|
|
||||||
const { verifyCode } = await import("../services/verification.js");
|
|
||||||
vi.mocked(verifyCode).mockResolvedValue({ status: "max_attempts" });
|
|
||||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "123456" });
|
|
||||||
expect(res.status).toBe(429);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 400 for invalid code", async () => {
|
|
||||||
const { verifyCode } = await import("../services/verification.js");
|
|
||||||
vi.mocked(verifyCode).mockResolvedValue({ status: "invalid" });
|
|
||||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "999999" });
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 200 with apiKey for valid code", async () => {
|
|
||||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "123456" });
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(res.body).toMatchObject({ status: "verified", apiKey: "free-key-123" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
11
src/__tests__/usage-free-tier-message.test.ts
Normal file
11
src/__tests__/usage-free-tier-message.test.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
describe("Usage middleware messaging", () => {
|
||||||
|
it("should not reference 'Free tier' in limit message", () => {
|
||||||
|
const usageSrc = readFileSync(join(__dirname, "../middleware/usage.ts"), "utf8");
|
||||||
|
// The rate limit message should say "Account limit" not "Free tier limit"
|
||||||
|
expect(usageSrc).not.toContain("Free tier limit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -132,7 +132,7 @@ describe("usage middleware", () => {
|
||||||
|
|
||||||
expect(mockStatus).toHaveBeenCalledWith(429);
|
expect(mockStatus).toHaveBeenCalledWith(429);
|
||||||
expect(mockJson).toHaveBeenCalledWith(
|
expect(mockJson).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ error: expect.stringContaining("Free tier limit") })
|
expect.objectContaining({ error: expect.stringContaining("Account limit") })
|
||||||
);
|
);
|
||||||
expect(mockNext).not.toHaveBeenCalled();
|
expect(mockNext).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export function usageMiddleware(req: any, res: any, next: any): void {
|
||||||
|
|
||||||
const record = usage.get(key);
|
const record = usage.get(key);
|
||||||
if (record && record.monthKey === monthKey && record.count >= FREE_TIER_LIMIT) {
|
if (record && record.monthKey === monthKey && record.count >= FREE_TIER_LIMIT) {
|
||||||
res.status(429).json({ error: "Free tier limit reached (100/month). Upgrade to Pro at https://docfast.dev/#pricing for 5,000 PDFs/month." });
|
res.status(429).json({ error: "Account limit reached (100/month). Upgrade to Pro at https://docfast.dev/#pricing for 5,000 PDFs/month." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
import { Router, Request, Response } from "express";
|
|
||||||
import rateLimit from "express-rate-limit";
|
|
||||||
import { createFreeKey } from "../services/keys.js";
|
|
||||||
import { createPendingVerification, verifyCode, isEmailVerified, getVerifiedApiKey } from "../services/verification.js";
|
|
||||||
import { sendVerificationEmail } from "../services/email.js";
|
|
||||||
import logger from "../services/logger.js";
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
const signupLimiter = rateLimit({
|
|
||||||
windowMs: 60 * 60 * 1000,
|
|
||||||
max: 5,
|
|
||||||
message: { error: "Too many signup attempts. Please try again in 1 hour." },
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const verifyLimiter = rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000,
|
|
||||||
max: 15,
|
|
||||||
message: { error: "Too many verification attempts. Please try again later." },
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function rejectDuplicateEmail(req: Request, res: Response, next: Function) {
|
|
||||||
const { email } = req.body || {};
|
|
||||||
if (email && typeof email === "string") {
|
|
||||||
const cleanEmail = email.trim().toLowerCase();
|
|
||||||
if (await isEmailVerified(cleanEmail)) {
|
|
||||||
res.status(409).json({ error: "Email already registered" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Request signup — generates 6-digit code, sends via email
|
|
||||||
router.post("/free", rejectDuplicateEmail, signupLimiter, async (req: Request, res: Response) => {
|
|
||||||
const { email } = req.body || {};
|
|
||||||
|
|
||||||
if (!email || typeof email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
||||||
res.status(400).json({ error: "A valid email address is required." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanEmail = email.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (await isEmailVerified(cleanEmail)) {
|
|
||||||
res.status(409).json({ error: "This email is already registered. Contact support if you need help." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pending = await createPendingVerification(cleanEmail);
|
|
||||||
|
|
||||||
sendVerificationEmail(cleanEmail, pending.code).catch(err => {
|
|
||||||
logger.error({ err, email: cleanEmail }, "Failed to send verification email");
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
status: "verification_required",
|
|
||||||
message: "Check your email for the verification code.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @openapi
|
|
||||||
* /v1/signup/verify:
|
|
||||||
* post:
|
|
||||||
* tags: [Account]
|
|
||||||
* summary: Verify email and get API key (discontinued)
|
|
||||||
* deprecated: true
|
|
||||||
* description: |
|
|
||||||
* **Discontinued.** Free accounts are no longer available. Try the demo at POST /v1/demo/html or upgrade to Pro at https://docfast.dev.
|
|
||||||
* Rate limited to 15 attempts per 15 minutes.
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* required: [email, code]
|
|
||||||
* properties:
|
|
||||||
* email:
|
|
||||||
* type: string
|
|
||||||
* format: email
|
|
||||||
* description: Email address used during signup
|
|
||||||
* example: user@example.com
|
|
||||||
* code:
|
|
||||||
* type: string
|
|
||||||
* description: 6-digit verification code from email
|
|
||||||
* example: "123456"
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Email verified, API key issued
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* status:
|
|
||||||
* type: string
|
|
||||||
* example: verified
|
|
||||||
* message:
|
|
||||||
* type: string
|
|
||||||
* apiKey:
|
|
||||||
* type: string
|
|
||||||
* description: The provisioned API key
|
|
||||||
* tier:
|
|
||||||
* type: string
|
|
||||||
* example: free
|
|
||||||
* 400:
|
|
||||||
* description: Missing fields or invalid verification code
|
|
||||||
* 409:
|
|
||||||
* description: Email already verified
|
|
||||||
* 410:
|
|
||||||
* description: Verification code expired
|
|
||||||
* 429:
|
|
||||||
* description: Too many failed attempts
|
|
||||||
*/
|
|
||||||
// Step 2: Verify code — creates API key
|
|
||||||
router.post("/verify", verifyLimiter, async (req: Request, res: Response) => {
|
|
||||||
const { email, code } = req.body || {};
|
|
||||||
|
|
||||||
if (!email || !code) {
|
|
||||||
res.status(400).json({ error: "Email and code are required." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanEmail = email.trim().toLowerCase();
|
|
||||||
const cleanCode = String(code).trim();
|
|
||||||
|
|
||||||
if (await isEmailVerified(cleanEmail)) {
|
|
||||||
res.status(409).json({ error: "This email is already verified." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await verifyCode(cleanEmail, cleanCode);
|
|
||||||
|
|
||||||
switch (result.status) {
|
|
||||||
case "ok": {
|
|
||||||
const keyInfo = await createFreeKey(cleanEmail);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
status: "verified",
|
|
||||||
message: "Email verified! Here's your API key.",
|
|
||||||
apiKey: keyInfo.key,
|
|
||||||
tier: keyInfo.tier,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "expired":
|
|
||||||
res.status(410).json({ error: "Verification code has expired. Please sign up again." });
|
|
||||||
break;
|
|
||||||
case "max_attempts":
|
|
||||||
res.status(429).json({ error: "Too many failed attempts. Please sign up again to get a new code." });
|
|
||||||
break;
|
|
||||||
case "invalid":
|
|
||||||
res.status(400).json({ error: "Invalid verification code." });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export { router as signupRouter };
|
|
||||||
|
|
@ -172,8 +172,8 @@ export async function initDatabase(): Promise<void> {
|
||||||
* - Unverified free-tier API keys (never completed verification)
|
* - Unverified free-tier API keys (never completed verification)
|
||||||
* - Orphaned usage rows (key no longer exists)
|
* - Orphaned usage rows (key no longer exists)
|
||||||
*/
|
*/
|
||||||
export async function cleanupStaleData(): Promise<{ expiredVerifications: number; staleKeys: number; orphanedUsage: number }> {
|
export async function cleanupStaleData(): Promise<{ expiredVerifications: number; orphanedUsage: number }> {
|
||||||
const results = { expiredVerifications: 0, staleKeys: 0, orphanedUsage: 0 };
|
const results = { expiredVerifications: 0, orphanedUsage: 0 };
|
||||||
|
|
||||||
// 1. Delete expired pending verifications
|
// 1. Delete expired pending verifications
|
||||||
const pv = await queryWithRetry(
|
const pv = await queryWithRetry(
|
||||||
|
|
@ -181,18 +181,7 @@ export async function cleanupStaleData(): Promise<{ expiredVerifications: number
|
||||||
);
|
);
|
||||||
results.expiredVerifications = pv.rowCount || 0;
|
results.expiredVerifications = pv.rowCount || 0;
|
||||||
|
|
||||||
// 2. Delete unverified free-tier keys (email not in verified verifications)
|
// 2. Delete orphaned usage rows (key no longer exists in api_keys)
|
||||||
const sk = await queryWithRetry(`
|
|
||||||
DELETE FROM api_keys
|
|
||||||
WHERE tier = 'free'
|
|
||||||
AND email NOT IN (
|
|
||||||
SELECT DISTINCT email FROM verifications WHERE verified_at IS NOT NULL
|
|
||||||
)
|
|
||||||
RETURNING key
|
|
||||||
`);
|
|
||||||
results.staleKeys = sk.rowCount || 0;
|
|
||||||
|
|
||||||
// 3. Delete orphaned usage rows
|
|
||||||
const ou = await queryWithRetry(`
|
const ou = await queryWithRetry(`
|
||||||
DELETE FROM usage
|
DELETE FROM usage
|
||||||
WHERE key NOT IN (SELECT key FROM api_keys)
|
WHERE key NOT IN (SELECT key FROM api_keys)
|
||||||
|
|
@ -202,7 +191,7 @@ export async function cleanupStaleData(): Promise<{ expiredVerifications: number
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{ ...results },
|
{ ...results },
|
||||||
`Database cleanup complete: ${results.expiredVerifications} expired verifications, ${results.staleKeys} stale keys, ${results.orphanedUsage} orphaned usage rows removed`
|
`Database cleanup complete: ${results.expiredVerifications} expired verifications, ${results.orphanedUsage} orphaned usage rows removed`
|
||||||
);
|
);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
|
||||||
|
|
@ -60,18 +60,3 @@ export async function verifyCode(email: string, code: string): Promise<{ status:
|
||||||
return { status: "ok" };
|
return { status: "ok" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isEmailVerified(email: string): Promise<boolean> {
|
|
||||||
const result = await queryWithRetry(
|
|
||||||
"SELECT 1 FROM verifications WHERE email = $1 AND verified_at IS NOT NULL LIMIT 1",
|
|
||||||
[email]
|
|
||||||
);
|
|
||||||
return result.rows.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getVerifiedApiKey(email: string): Promise<string | null> {
|
|
||||||
const result = await queryWithRetry(
|
|
||||||
"SELECT api_key FROM verifications WHERE email = $1 AND verified_at IS NOT NULL LIMIT 1",
|
|
||||||
[email]
|
|
||||||
);
|
|
||||||
return result.rows[0]?.api_key ?? null;
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue