feat(api): rate limiting
This commit is contained in:
parent
0ce950c9c8
commit
207d3483ca
2
TODO.md
2
TODO.md
@ -36,7 +36,7 @@
|
||||
- [x] Setup tests with database + add coverage
|
||||
- [ ] Setup HTTP Requests logging in development (not needed in `test` mode)
|
||||
- [ ] Setup Health checks
|
||||
- [ ] Setup Rate limiting
|
||||
- [x] Setup Rate limiting
|
||||
- [ ] Share VineJS validators between `website` and `api`
|
||||
- [ ] Implement Wikipedia Game Solver (`website`)
|
||||
- [x] Init Next.js project
|
||||
|
@ -10,3 +10,5 @@ DATABASE_PASSWORD=password
|
||||
DATABASE_NAME=wikipedia
|
||||
DATABASE_HOST=127.0.0.1
|
||||
DATABASE_PORT=5432
|
||||
|
||||
LIMITER_STORE=database
|
||||
|
@ -19,6 +19,7 @@
|
||||
"@adonisjs/core": "catalog:",
|
||||
"@adonisjs/cors": "catalog:",
|
||||
"@adonisjs/lucid": "catalog:",
|
||||
"@adonisjs/limiter": "catalog:",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@repo/wikipedia-game-solver": "workspace:*",
|
||||
"@vinejs/vine": "catalog:",
|
||||
|
@ -34,6 +34,9 @@ export default defineConfig({
|
||||
async () => {
|
||||
return await import("@adonisjs/auth/auth_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/limiter/limiter_provider")
|
||||
},
|
||||
],
|
||||
preloads: [
|
||||
async () => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Page from "#app/models/page.js"
|
||||
import { throttle } from "#start/limiter.js"
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import router from "@adonisjs/core/services/router"
|
||||
import { sanitizePageTitle } from "@repo/wikipedia-game-solver/wikipedia-utils"
|
||||
@ -33,4 +34,4 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/wikipedia/pages", [Controller])
|
||||
router.get("/wikipedia/pages", [Controller]).use(throttle)
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
process.env["NODE_ENV"] = "test"
|
||||
process.env["PORT"] = "3333"
|
||||
process.env["LIMITER_STORE"] = "memory"
|
||||
|
||||
import { Ignitor, prettyPrintError } from "@adonisjs/core"
|
||||
import { configure, processCLIArgs, run } from "@japa/runner"
|
||||
|
26
apps/api/src/config/limiter.ts
Normal file
26
apps/api/src/config/limiter.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import env from "#start/env.js"
|
||||
import { defineConfig, stores } from "@adonisjs/limiter"
|
||||
|
||||
const limiterConfig = defineConfig({
|
||||
default: env.get("LIMITER_STORE"),
|
||||
stores: {
|
||||
/**
|
||||
* Database store to save rate limiting data inside a database.
|
||||
*/
|
||||
database: stores.database({
|
||||
tableName: "rate_limits",
|
||||
clearExpiredByTimeout: true,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Memory store could be used during testing.
|
||||
*/
|
||||
memory: stores.memory({}),
|
||||
},
|
||||
})
|
||||
|
||||
export default limiterConfig
|
||||
|
||||
declare module "@adonisjs/limiter/types" {
|
||||
export interface LimitersList extends InferLimiters<typeof limiterConfig> {}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { BaseSchema } from "@adonisjs/lucid/schema"
|
||||
|
||||
export default class CreateRateLimitsTable extends BaseSchema {
|
||||
protected tableName = "rate_limits"
|
||||
|
||||
public override async up(): Promise<void> {
|
||||
void this.schema.createTable(this.tableName, (table) => {
|
||||
table.string("key", 255).notNullable().primary()
|
||||
table.integer("points", 9).notNullable().defaultTo(0)
|
||||
table.bigint("expire").unsigned()
|
||||
})
|
||||
}
|
||||
|
||||
public override async down(): Promise<void> {
|
||||
void this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
@ -19,11 +19,16 @@ export default await Env.create(new URL("../..", import.meta.url), {
|
||||
] as const),
|
||||
|
||||
/**
|
||||
* Variables for configuring database connection
|
||||
* Variables for configuring database connection.
|
||||
*/
|
||||
DATABASE_HOST: Env.schema.string({ format: "host" }),
|
||||
DATABASE_PORT: Env.schema.number(),
|
||||
DATABASE_USER: Env.schema.string(),
|
||||
DATABASE_PASSWORD: Env.schema.string(),
|
||||
DATABASE_NAME: Env.schema.string(),
|
||||
|
||||
/**
|
||||
* Variables for configuring the limiter package.
|
||||
*/
|
||||
LIMITER_STORE: Env.schema.enum(["database", "memory"] as const),
|
||||
})
|
||||
|
15
apps/api/src/start/limiter.ts
Normal file
15
apps/api/src/start/limiter.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Define HTTP limiters
|
||||
*
|
||||
* The "limiter.define" method creates an HTTP middleware to apply rate limits on a route or a group of routes. Feel free to define as many throttle middleware as needed.
|
||||
*/
|
||||
|
||||
import app from "@adonisjs/core/services/app"
|
||||
import limiter from "@adonisjs/limiter/services/main"
|
||||
|
||||
export const throttle = limiter.define("global", () => {
|
||||
if (app.inTest) {
|
||||
return limiter.noLimit()
|
||||
}
|
||||
return limiter.allowRequests(120).every("1 minute")
|
||||
})
|
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
@ -18,6 +18,9 @@ catalogs:
|
||||
'@adonisjs/cors':
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1
|
||||
'@adonisjs/limiter':
|
||||
specifier: 2.3.2
|
||||
version: 2.3.2
|
||||
'@adonisjs/lucid':
|
||||
specifier: 21.2.0
|
||||
version: 21.2.0
|
||||
@ -275,6 +278,9 @@ importers:
|
||||
'@adonisjs/cors':
|
||||
specifier: 'catalog:'
|
||||
version: 2.2.1(@adonisjs/core@6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0))
|
||||
'@adonisjs/limiter':
|
||||
specifier: 'catalog:'
|
||||
version: 2.3.2(@adonisjs/core@6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0))(@adonisjs/lucid@21.2.0(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@adonisjs/core@6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0))(better-sqlite3@11.1.2)(luxon@3.5.0)(pg@8.12.0))
|
||||
'@adonisjs/lucid':
|
||||
specifier: 'catalog:'
|
||||
version: 21.2.0(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@adonisjs/core@6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0))(better-sqlite3@11.1.2)(luxon@3.5.0)(pg@8.12.0)
|
||||
@ -399,7 +405,7 @@ importers:
|
||||
version: 14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-intl:
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.2(next@14.2.5(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 3.17.2(next@14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
next-themes:
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -448,7 +454,7 @@ importers:
|
||||
version: 8.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(typescript@5.5.4)
|
||||
'@storybook/test':
|
||||
specifier: 'catalog:'
|
||||
version: 8.2.8(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(vitest@2.0.5(@types/node@22.2.0)(@vitest/browser@2.0.5)(@vitest/ui@2.0.5)(terser@5.31.5))
|
||||
version: 8.2.8(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(vitest@2.0.5(@types/node@22.2.0))
|
||||
'@storybook/test-runner':
|
||||
specifier: 'catalog:'
|
||||
version: 0.19.1(@swc/helpers@0.5.5)(@types/node@22.2.0)(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))
|
||||
@ -514,7 +520,7 @@ importers:
|
||||
version: 14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-intl:
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.2(next@14.2.5(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 3.17.2(next@14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 18.3.1
|
||||
@ -629,7 +635,7 @@ importers:
|
||||
version: 14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-intl:
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.2(next@14.2.5(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 3.17.2(next@14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 18.3.1
|
||||
@ -730,7 +736,7 @@ importers:
|
||||
version: 14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-intl:
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.2(next@14.2.5(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 3.17.2(next@14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
next-themes:
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -833,7 +839,7 @@ importers:
|
||||
version: 14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-intl:
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.2(next@14.2.5(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 3.17.2(next@14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 18.3.1
|
||||
@ -858,7 +864,7 @@ importers:
|
||||
version: 8.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(typescript@5.5.4)
|
||||
'@storybook/test':
|
||||
specifier: 'catalog:'
|
||||
version: 8.2.8(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(vitest@2.0.5(@types/node@22.2.0)(@vitest/browser@2.0.5)(@vitest/ui@2.0.5)(terser@5.31.5))
|
||||
version: 8.2.8(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(vitest@2.0.5(@types/node@22.2.0))
|
||||
'@total-typescript/ts-reset':
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.1
|
||||
@ -1017,6 +1023,18 @@ packages:
|
||||
'@adonisjs/fold': ^10.0.1
|
||||
'@adonisjs/logger': ^6.0.1
|
||||
|
||||
'@adonisjs/limiter@2.3.2':
|
||||
resolution: {integrity: sha512-6DAzJl9c7XGPEakge35Dr7+yqqrpWkifQDKm3cwl/+WaxBPvxWbJBTjzu+P9NRCiIofqfH89QFeXWpJN1+dl0w==}
|
||||
peerDependencies:
|
||||
'@adonisjs/core': ^6.12.1
|
||||
'@adonisjs/lucid': ^20.1.0 || ^21.0.0
|
||||
'@adonisjs/redis': ^8.0.1 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
'@adonisjs/lucid':
|
||||
optional: true
|
||||
'@adonisjs/redis':
|
||||
optional: true
|
||||
|
||||
'@adonisjs/logger@6.0.3':
|
||||
resolution: {integrity: sha512-CKxIpWBEX/e6duRE6qq8GJ90NQC8q26Q0aSuj+bUO6X4mgcgawxhciJTfpxmJNj9KEUmNAeHOn0hSpTITdk8Lg==}
|
||||
engines: {node: '>=18.16.0'}
|
||||
@ -7887,6 +7905,9 @@ packages:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
rate-limiter-flexible@5.0.3:
|
||||
resolution: {integrity: sha512-lWx2y8NBVlTOLPyqs+6y7dxfEpT6YFqKy3MzWbCy95sTTOhOuxufP2QvRyOHpfXpB9OUJPbVLybw3z3AVAS5fA==}
|
||||
|
||||
raw-body@2.5.2:
|
||||
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -9631,6 +9652,13 @@ snapshots:
|
||||
vary: 1.1.2
|
||||
youch: 3.3.3
|
||||
|
||||
'@adonisjs/limiter@2.3.2(@adonisjs/core@6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0))(@adonisjs/lucid@21.2.0(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@adonisjs/core@6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0))(better-sqlite3@11.1.2)(luxon@3.5.0)(pg@8.12.0))':
|
||||
dependencies:
|
||||
'@adonisjs/core': 6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0)
|
||||
rate-limiter-flexible: 5.0.3
|
||||
optionalDependencies:
|
||||
'@adonisjs/lucid': 21.2.0(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@adonisjs/core@6.12.1(@adonisjs/assembler@7.7.0(typescript@5.5.4))(@vinejs/vine@2.1.0))(better-sqlite3@11.1.2)(luxon@3.5.0)(pg@8.12.0)
|
||||
|
||||
'@adonisjs/logger@6.0.3':
|
||||
dependencies:
|
||||
'@poppinss/utils': 6.7.3
|
||||
@ -12260,30 +12288,12 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@storybook/test@8.2.8(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(vitest@2.0.5(@types/node@22.2.0)(@vitest/browser@2.0.5)(@vitest/ui@2.0.5)(terser@5.31.5))':
|
||||
dependencies:
|
||||
'@storybook/csf': 0.1.11
|
||||
'@storybook/instrumenter': 8.2.8(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))
|
||||
'@testing-library/dom': 10.1.0
|
||||
'@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(vitest@2.0.5(@types/node@22.2.0)(@vitest/browser@2.0.5)(@vitest/ui@2.0.5)(terser@5.31.5))
|
||||
'@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0)
|
||||
'@vitest/expect': 1.6.0
|
||||
'@vitest/spy': 1.6.0
|
||||
storybook: 8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2))
|
||||
util: 0.12.5
|
||||
transitivePeerDependencies:
|
||||
- '@jest/globals'
|
||||
- '@types/bun'
|
||||
- '@types/jest'
|
||||
- jest
|
||||
- vitest
|
||||
|
||||
'@storybook/test@8.2.8(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))(vitest@2.0.5(@types/node@22.2.0))':
|
||||
dependencies:
|
||||
'@storybook/csf': 0.1.11
|
||||
'@storybook/instrumenter': 8.2.8(storybook@8.2.8(@babel/preset-env@7.25.3(@babel/core@7.25.2)))
|
||||
'@testing-library/dom': 10.1.0
|
||||
'@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(vitest@2.0.5(@types/node@22.2.0)(@vitest/browser@2.0.5)(@vitest/ui@2.0.5)(terser@5.31.5))
|
||||
'@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(vitest@2.0.5(@types/node@22.2.0))
|
||||
'@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0)
|
||||
'@vitest/expect': 1.6.0
|
||||
'@vitest/spy': 1.6.0
|
||||
@ -12405,7 +12415,7 @@ snapshots:
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(vitest@2.0.5(@types/node@22.2.0)(@vitest/browser@2.0.5)(@vitest/ui@2.0.5)(terser@5.31.5))':
|
||||
'@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(jest@29.7.0(@types/node@22.2.0))(vitest@2.0.5(@types/node@22.2.0))':
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.4.0
|
||||
'@babel/runtime': 7.25.0
|
||||
@ -16896,7 +16906,7 @@ snapshots:
|
||||
|
||||
nerf-dart@1.0.0: {}
|
||||
|
||||
next-intl@3.17.2(next@14.2.5(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
next-intl@3.17.2(next@14.2.5(@babel/core@7.25.2)(@playwright/test@1.46.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.2.32
|
||||
negotiator: 0.6.3
|
||||
@ -17734,6 +17744,8 @@ snapshots:
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
rate-limiter-flexible@5.0.3: {}
|
||||
|
||||
raw-body@2.5.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
|
@ -29,6 +29,7 @@ catalog:
|
||||
"@adonisjs/core": "6.12.1"
|
||||
"@adonisjs/cors": "2.2.1"
|
||||
"@adonisjs/lucid": "21.2.0"
|
||||
"@adonisjs/limiter": "2.3.2"
|
||||
"pg": "8.12.0"
|
||||
"better-sqlite3": "11.1.2"
|
||||
"@adonisjs/assembler": "7.7.0"
|
||||
|
Reference in New Issue
Block a user