Skip to content

Commit b90b5a1

Browse files
committed
feat add support for urls
1 parent 4ffab07 commit b90b5a1

File tree

10 files changed

+121
-3
lines changed

10 files changed

+121
-3
lines changed

src/module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface ModuleOptions {
1818
errorMessage: string;
1919
retryAfterHeader: boolean;
2020
log: false | LogEntry;
21+
routes: string[];
2122
}
2223

2324
export default defineNuxtModule<ModuleOptions>({
@@ -35,6 +36,7 @@ export default defineNuxtModule<ModuleOptions>({
3536
errorMessage: "Too Many Requests",
3637
retryAfterHeader: false,
3738
log: false,
39+
routes: [],
3840
},
3941
setup(options, nuxt) {
4042
const resolver = createResolver(import.meta.url);

src/runtime/server/middleware/shield.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ import { isBanExpired } from "../utils/isBanExpired";
1010
import shieldLog from "../utils/shieldLog";
1111

1212
export default defineEventHandler(async (event) => {
13-
if (!event.node.req.url?.startsWith("/api/")) {
13+
const config = useRuntimeConfig().public.nuxtApiShield;
14+
const url = event.node.req.url;
15+
16+
if (!url?.startsWith("/api/") || (config.routes?.length && !config.routes.some(route => url.startsWith(route)))) {
1417
return;
1518
}
1619

20+
1721
// console.log(
1822
// `👉 Handling request for URL: ${event.node.req.url} from IP: ${
1923
// getRequestIP(event, { xForwardedFor: true }) || "unKnownIP"

test/basic.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const nuxtConfigBan = 10;
1111
beforeEach(async () => {
1212
// await useStorage("shield").clear(); TODO waiting for https://github.com/nuxt/test-utils/issues/531
1313
// this is a workaround to clean the storage
14-
const storagePath = fileURLToPath(new URL("../_testShield", import.meta.url));
14+
const storagePath = fileURLToPath(new URL("../_testBasciShield", import.meta.url));
1515
await rm(storagePath, { recursive: true, force: true });
1616
});
1717

test/fixtures/basic/nuxt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default defineNuxtConfig({
2020
shield: {
2121
//driver: "memory",
2222
driver: "fs",
23-
base: "_testShield",
23+
base: "_testBasicShield",
2424
},
2525
},
2626
},

test/fixtures/withroutes/app.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
<div>withRoutes</div>
3+
</template>
4+
5+
<script setup></script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import nuxtApiShield from "../../../src/module";
2+
3+
export default defineNuxtConfig({
4+
modules: [nuxtApiShield],
5+
nuxtApiShield: {
6+
limit: {
7+
max: 2,
8+
duration: 3,
9+
ban: 10,
10+
},
11+
errorMessage: "Leave me alone",
12+
retryAfterHeader: true,
13+
log: false,
14+
routes: ["/api/v3"],
15+
},
16+
nitro: {
17+
storage: {
18+
shield: {
19+
//driver: "memory",
20+
driver: "fs",
21+
base: "_testWithRoutesShield",
22+
},
23+
},
24+
},
25+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"private": true,
3+
"name": "withroutes",
4+
"type": "module"
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default defineEventHandler(async () => {
2+
return { id: 1, name: "Gauranga" };
3+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default defineEventHandler(async () => {
2+
return { id: 1, name: "Gauranga" };
3+
});

test/withroutes.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, it, expect } from "vitest";
2+
import { fileURLToPath } from "node:url";
3+
import { setup, $fetch } from "@nuxt/test-utils/e2e";
4+
import { beforeEach } from "vitest";
5+
import { rm } from "node:fs/promises";
6+
7+
beforeEach(async () => {
8+
// await useStorage("shield").clear(); TODO waiting for https://github.com/nuxt/test-utils/issues/531
9+
// this is a workaround to clean the storage
10+
const storagePath = fileURLToPath(new URL("../_testWithRoutesShield", import.meta.url));
11+
await rm(storagePath, { recursive: true, force: true });
12+
});
13+
14+
describe("shield", async () => {
15+
await setup({
16+
rootDir: fileURLToPath(new URL("./fixtures/withroutes", import.meta.url)),
17+
});
18+
19+
it("respond to api call 2 times (limit.max, limit.duration) and rejects the 3rd call if the route matches the routes option", async () => {
20+
// req.count = 1
21+
let response = await $fetch("/api/v3/example?c=1/1", {
22+
method: "GET",
23+
retryStatusCodes: [],
24+
});
25+
expect((response as any).name).toBe("Gauranga");
26+
27+
// req.count = 2
28+
response = await $fetch("/api/v3/example?c=1/2", {
29+
method: "GET",
30+
retryStatusCodes: [],
31+
});
32+
expect((response as any).name).toBe("Gauranga");
33+
34+
try {
35+
// req.count = 3
36+
// as limit.max = 2, this should throw 429 and ban for 3 seconds (limit.ban)
37+
expect(async () =>
38+
$fetch("/api/v3/example?c=1/3", { method: "GET", retryStatusCodes: [] })
39+
).rejects.toThrowError();
40+
} catch (err) {
41+
const typedErr = err as { statusCode: number; statusMessage: string };
42+
expect(typedErr.statusCode).toBe(429);
43+
expect(typedErr.statusMessage).toBe("Leave me alone");
44+
}
45+
});
46+
47+
it("respond to api call 2 times (limit.max, limit.duration) and accept the 3rd call if the route does not matches the routes option", async () => {
48+
// req.count = 1
49+
let response = await $fetch("/api/v2/example?c=2/1", {
50+
method: "GET",
51+
retryStatusCodes: [],
52+
});
53+
expect((response as any).name).toBe("Gauranga");
54+
55+
// req.count = 2
56+
response = await $fetch("/api/v2/example?c=2/2", {
57+
method: "GET",
58+
retryStatusCodes: [],
59+
});
60+
expect((response as any).name).toBe("Gauranga");
61+
62+
// req.count = 3
63+
response = await $fetch("/api/v2/example?c=2/3", {
64+
method: "GET",
65+
retryStatusCodes: [],
66+
});
67+
expect((response as any).name).toBe("Gauranga");
68+
});
69+
70+
71+
});

0 commit comments

Comments
 (0)