diff --git a/.gitignore b/.gitignore index b0c5634..5e29594 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,3 @@ coverage/ # db *.db - -# auth private key -auth_key.pem \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 31cebd5..f1d0329 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "dotenv": "^17.2.3", "express": "^5.1.0", - "uuid": "^13.0.0" + "express-rate-limit": "^8.1.0" }, "devDependencies": { "@types/express": "^5.0.3", @@ -1122,6 +1122,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", + "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1462,6 +1480,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2439,19 +2466,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index b9d8d0d..dbb5598 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dependencies": { "dotenv": "^17.2.3", "express": "^5.1.0", - "uuid": "^13.0.0" + "express-rate-limit": "^8.1.0" }, "devDependencies": { "@types/express": "^5.0.3", diff --git a/src/auth.ts b/src/auth.ts deleted file mode 100644 index 9d568ea..0000000 --- a/src/auth.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { constants, createPrivateKey, privateDecrypt } from 'crypto'; -import * as fs from 'fs'; -import { validate as validateUUID } from 'uuid'; - -/** - * Simple auth implementation using RSA. - * Client sends a base64 UUID encrypted with public key, server decrypts with private key and checks validity. - */ -export class Auth { - constructor() {} - - decryptToken(token: string): string { - const keyPem = fs.readFileSync('auth_key.pem', 'utf-8'); - const privateKey = createPrivateKey({ key: keyPem, format: 'pem' }); - const encryptedBuffer = Buffer.from(token, 'base64'); - const decrypted = privateDecrypt( - { - key: privateKey, - padding: constants.RSA_PKCS1_PADDING, - }, - encryptedBuffer - ); - return decrypted.toString('utf-8'); - } - - isTokenValid(decryptedToken: string): boolean { - return validateUUID(decryptedToken); - } - - isTokenExpired(issuedAt: number): boolean { - const tokenLifetimeSeconds = 300; - const allowedDriftSeconds = 10; - const now = Math.floor(Date.now() / 1000); // current unix timestamp in seconds - const diff = now - issuedAt; - return ( - diff < -allowedDriftSeconds || - diff > tokenLifetimeSeconds + allowedDriftSeconds - ); - } -} diff --git a/src/authMiddleware.ts b/src/authMiddleware.ts deleted file mode 100644 index b7fb1b0..0000000 --- a/src/authMiddleware.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { NextFunction, Request, Response } from 'express'; -import { Auth } from './auth.js'; -import { AuthPayload } from './types.js'; - -const auth = new Auth(); - -export function bearerAuthMiddleware( - req: Request, - res: Response, - next: NextFunction -) { - const authHeader = req.headers['authorization']; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res - .status(401) - .json({ error: 'Missing or invalid Authorization header' }); - } - - try { - const token = authHeader.substring('Bearer '.length); - const decrypted = auth.decryptToken(token); - if (!decrypted) { - return res.status(401).json({ error: 'Invalid token' }); - } - - const payload: AuthPayload = JSON.parse(decrypted); - if (!payload.token || typeof payload.issuedAt !== 'number') { - return res.status(401).json({ error: 'Token payload missing fields' }); - } - if (!auth.isTokenValid(payload.token)) { - return res.status(401).json({ error: 'Token is not a valid UUID' }); - } - if (auth.isTokenExpired(payload.issuedAt)) { - return res.status(401).json({ error: 'Token expired' }); - } - next(); - } catch (e) { - return res.status(500).json({ error: (e as Error).message }); - } -} diff --git a/src/index.ts b/src/index.ts index 98a8339..da60cd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; import express from 'express'; -import { bearerAuthMiddleware } from './authMiddleware.js'; +import { rateLimit } from 'express-rate-limit'; import { onGetImages, onGetQuoteById, @@ -10,7 +10,25 @@ import { const app = express(); -app.use(bearerAuthMiddleware); +const hourlyLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 1000, + message: 'Too many requests, please try again in an hour', + standardHeaders: true, + legacyHeaders: false, +}); + +const burstLimiter = rateLimit({ + windowMs: 10 * 1000, + max: 50, + message: 'Slow down! Too many requests', + skipSuccessfulRequests: true, + standardHeaders: true, + legacyHeaders: false, +}); + +app.use(hourlyLimiter); +app.use(burstLimiter); app.get('/quotes/random', onGetRandomQuote); app.get('/quotes/:id', onGetQuoteById); diff --git a/src/types.d.ts b/src/types.d.ts index 8698233..00498c0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,12 +1,3 @@ -export interface AuthPayload { - // An UUID string, unique per request. - token: string; - - // Unix time in seconds. It is the time when the token was issued. - // Will expire after a time window. - issuedAt: number; -} - export interface Quote { id: number; text: string;