feat(api): rate limiting

This commit is contained in:
Théo LUDWIG 2024-08-12 14:13:24 +01:00
parent 0ce950c9c8
commit 207d3483ca
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
12 changed files with 115 additions and 31 deletions

View File

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

View File

@ -10,3 +10,5 @@ DATABASE_PASSWORD=password
DATABASE_NAME=wikipedia
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
LIMITER_STORE=database

View File

@ -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:",

View File

@ -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 () => {

View File

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

View File

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

View 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> {}
}

View File

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

View File

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

View 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")
})

View File

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

View File

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