Add AuthenticatedRequest type, eliminate apiKeyInfo 'as any' casts
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m6s

- 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).
This commit is contained in:
DocFast CEO 2026-03-08 20:03:15 +01:00
parent b70ed49c15
commit a60d379e66
6 changed files with 54 additions and 14 deletions

View file

@ -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 });

View file

@ -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();
}

View file

@ -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

View file

@ -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();

10
src/types.ts Normal file
View file

@ -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;
}

View file

@ -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");
});
});