From a60d379e667b00ba11e98b66dfc4337fe8028f7f Mon Sep 17 00:00:00 2001 From: DocFast CEO Date: Sun, 8 Mar 2026 20:03:15 +0100 Subject: [PATCH] Add AuthenticatedRequest type, eliminate apiKeyInfo 'as any' casts - Created src/types.ts with AuthenticatedRequest interface extending Express Request - Replaced (req as any).apiKeyInfo with typed AuthenticatedRequest cast in: - auth.ts, usage.ts, pdfRateLimit.ts middleware - index.ts route handlers (usage/me, admin auth, admin usage, admin cleanup, concurrency) - 4 TDD tests added. 566 tests passing (50 files). --- src/index.ts | 19 +++++++++-------- src/middleware/auth.ts | 3 ++- src/middleware/pdfRateLimit.ts | 5 +++-- src/middleware/usage.ts | 6 ++++-- src/types.ts | 10 +++++++++ tests/types-authenticated-request.test.ts | 25 +++++++++++++++++++++++ 6 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 src/types.ts create mode 100644 tests/types-authenticated-request.test.ts diff --git a/src/index.ts b/src/index.ts index 7dd595e..2f31c02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ -import express from "express"; +import express, { Request, Response, NextFunction } from "express"; import { randomUUID } from "crypto"; +import { AuthenticatedRequest } from "./types.js"; import { createRequire } from "module"; import { compressionMiddleware } from "./middleware/compression.js"; import logger from "./services/logger.js"; @@ -183,8 +184,8 @@ app.use("/v1/templates", defaultJsonParser, authMiddleware, usageMiddleware, tem * 401: * description: Missing or invalid API key */ -app.get("/v1/usage/me", authMiddleware, (req: any, res: any) => { - const key = req.apiKeyInfo.key; +app.get("/v1/usage/me", authMiddleware, (req: Request, res: Response) => { + const key = (req as AuthenticatedRequest).apiKeyInfo.key; const { count, monthKey } = getUsageForKey(key); const pro = isProKey(key); res.json({ @@ -196,23 +197,23 @@ app.get("/v1/usage/me", authMiddleware, (req: any, res: any) => { }); // Admin: usage stats (admin key required) -const adminAuth = (req: any, res: any, next: any) => { +const adminAuth = (req: Request, res: Response, next: NextFunction) => { const adminKey = process.env.ADMIN_API_KEY; if (!adminKey) { res.status(503).json({ error: "Admin access not configured" }); return; } - if (req.apiKeyInfo?.key !== adminKey) { res.status(403).json({ error: "Admin access required" }); return; } + if ((req as AuthenticatedRequest).apiKeyInfo?.key !== adminKey) { res.status(403).json({ error: "Admin access required" }); return; } next(); }; -app.get("/v1/usage", authMiddleware, adminAuth, (req: any, res: any) => { - res.json(getUsageStats(req.apiKeyInfo?.key)); +app.get("/v1/usage", authMiddleware, adminAuth, (req: Request, res: Response) => { + res.json(getUsageStats((req as AuthenticatedRequest).apiKeyInfo?.key)); }); // Admin: concurrency stats (admin key required) -app.get("/v1/concurrency", authMiddleware, adminAuth, (_req: any, res: any) => { +app.get("/v1/concurrency", authMiddleware, adminAuth, (_req: Request, res: Response) => { res.json(getConcurrencyStats()); }); // Admin: database cleanup (admin key required) -app.post("/admin/cleanup", authMiddleware, adminAuth, async (_req: any, res: any) => { +app.post("/admin/cleanup", authMiddleware, adminAuth, async (_req: Request, res: Response) => { try { const results = await cleanupStaleData(); res.json({ status: "ok", cleaned: results }); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 3748b1b..12d599d 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { isValidKey, getKeyInfo } from "../services/keys.js"; +import { AuthenticatedRequest } from "../types.js"; export function authMiddleware( req: Request, @@ -25,6 +26,6 @@ export function authMiddleware( return; } // Attach key info to request for downstream use - (req as any).apiKeyInfo = getKeyInfo(key); + (req as AuthenticatedRequest).apiKeyInfo = getKeyInfo(key)!; next(); } diff --git a/src/middleware/pdfRateLimit.ts b/src/middleware/pdfRateLimit.ts index 0d64d89..30005de 100644 --- a/src/middleware/pdfRateLimit.ts +++ b/src/middleware/pdfRateLimit.ts @@ -1,6 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { isProKey } from "../services/keys.js"; import logger from "../services/logger.js"; +import { AuthenticatedRequest } from "../types.js"; interface RateLimitEntry { count: number; @@ -117,8 +118,8 @@ function releaseConcurrencySlot(): void { } } -export function pdfRateLimitMiddleware(req: Request & { apiKeyInfo?: any }, res: Response, next: NextFunction): void { - const keyInfo = req.apiKeyInfo; +export function pdfRateLimitMiddleware(req: Request, res: Response, next: NextFunction): void { + const keyInfo = (req as AuthenticatedRequest).apiKeyInfo; const apiKey = keyInfo?.key || "unknown"; // Check rate limit first diff --git a/src/middleware/usage.ts b/src/middleware/usage.ts index dda1428..f3893af 100644 --- a/src/middleware/usage.ts +++ b/src/middleware/usage.ts @@ -1,7 +1,9 @@ +import { Request, Response, NextFunction } from "express"; import { isProKey } from "../services/keys.js"; import logger from "../services/logger.js"; import pool from "../services/db.js"; import { queryWithRetry, connectWithRetry } from "../services/db.js"; +import { AuthenticatedRequest } from "../types.js"; const FREE_TIER_LIMIT = 100; const PRO_TIER_LIMIT = 5000; @@ -76,8 +78,8 @@ setInterval(flushDirtyEntries, FLUSH_INTERVAL_MS); // Note: SIGTERM/SIGINT flush is handled by the shutdown orchestrator in index.ts // to avoid race conditions with pool.end(). -export function usageMiddleware(req: any, res: any, next: any): void { - const keyInfo = req.apiKeyInfo; +export function usageMiddleware(req: Request, res: Response, next: NextFunction): void { + const keyInfo = (req as AuthenticatedRequest).apiKeyInfo; const key = keyInfo?.key || "unknown"; const monthKey = getMonthKey(); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ca4ab70 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,10 @@ +import { Request } from "express"; +import { ApiKey } from "./services/keys.js"; + +/** + * Express Request with authenticated API key info attached by authMiddleware. + * Use this instead of `(req as any).apiKeyInfo` casts. + */ +export interface AuthenticatedRequest extends Request { + apiKeyInfo: ApiKey; +} diff --git a/tests/types-authenticated-request.test.ts b/tests/types-authenticated-request.test.ts new file mode 100644 index 0000000..96c8da6 --- /dev/null +++ b/tests/types-authenticated-request.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from "vitest"; + +describe("AuthenticatedRequest type", () => { + it("should be importable from types module", async () => { + const mod = await import("../src/types.js"); + // Type exists — this is a compile-time check; runtime just verifies module loads + expect(mod).toBeDefined(); + }); + + it("should allow apiKeyInfo on typed request in auth middleware", async () => { + // Verify auth middleware compiles without 'as any' cast + const authMod = await import("../src/middleware/auth.js"); + expect(authMod.authMiddleware).toBeTypeOf("function"); + }); + + it("should allow usage middleware to access apiKeyInfo without any cast", async () => { + const usageMod = await import("../src/middleware/usage.js"); + expect(usageMod.usageMiddleware).toBeTypeOf("function"); + }); + + it("should allow pdfRateLimit middleware to use AuthenticatedRequest", async () => { + const mod = await import("../src/middleware/pdfRateLimit.js"); + expect(mod.pdfRateLimitMiddleware).toBeTypeOf("function"); + }); +});