Skip to content

Commit baf5f61

Browse files
committed
refactor(examples): update nextjs examples to better align with last documentation
1 parent e0cfb73 commit baf5f61

File tree

19 files changed

+867
-174
lines changed

19 files changed

+867
-174
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,8 @@ dist
130130
.yarn/install-state.gz
131131
.pnp.*
132132

133-
# CLaude
133+
# Claude
134134
.claude
135+
136+
# History
137+
.history

examples/nextjs/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
LAPI_URL='http://localhost:8080'
22
TEST_LOG_LEVEL='debug'
33

4+
## CrowdSec
5+
BOUNCER_KEY='1234'
6+
BOUNCED_IP='172.19.0.1'

examples/nextjs/README.md

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,61 +10,67 @@ It aims to help developers to understand how to integrate CrowdSec remediation i
1010
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
1111
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
1212

13-
- [Technical overview](#technical-overview)
14-
- [Middleware (`src/middleware.ts`)](#middleware-srcmiddlewarets)
15-
- [API Route (`src/app/api/crowdsec/route.ts`)](#api-route-srcappapicrowdsecroutets)
16-
- [Captcha Handler (`src/app/crowdsec-captcha/route.ts`)](#captcha-handler-srcappcrowdsec-captcharoutets)
17-
- [Test the bouncer](#test-the-bouncer)
18-
- [Pre-requisites](#pre-requisites)
19-
- [Prepare the tests](#prepare-the-tests)
20-
- [Test a "bypass" remediation](#test-a-bypass-remediation)
21-
- [Test a "ban" remediation](#test-a-ban-remediation)
22-
- [Test a "captcha" remediation](#test-a-captcha-remediation)
13+
- [NextJS basic implementation](#nextjs-basic-implementation)
14+
- [Technical overview](#technical-overview)
15+
- [Middleware (`src/middleware.ts`)](#middleware-srcmiddlewarets)
16+
- [API Routes](#api-routes)
17+
- [Remediation Check (`src/app/api/crowdsec/remediation/route.ts`)](#remediation-check-srcappapicrowdsecremediationroutets)
18+
- [Captcha Handler (`src/app/api/crowdsec/captcha/route.ts`)](#captcha-handler-srcappapicrowdseccaptcharoutets)
19+
- [Test the bouncer](#test-the-bouncer)
20+
- [Pre-requisites](#pre-requisites)
21+
- [Prepare the tests](#prepare-the-tests)
22+
- [Test a "bypass" remediation](#test-a-bypass-remediation)
23+
- [Test a "ban" remediation](#test-a-ban-remediation)
24+
- [Test a "captcha" remediation](#test-a-captcha-remediation)
2325

2426
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
2527

2628
## Technical overview
2729

2830
The implementation uses Next.js App Router with middleware and API routes.
2931

30-
**Important Note**: We cannot use the CrowdSec bouncer directly in the Next.js middleware because middleware runs on the Edge Runtime, which doesn't have access to Node.js APIs that the bouncer requires. While Next.js offers an experimental `nodeMiddleware` feature that would allow Node.js APIs in middleware, we prefer not to rely on experimental features for production use. Instead, we use a custom API route (`/api/crowdsec`) that runs in the Node.js runtime and can access the full bouncer functionality.
32+
**Important Note**: Starting from Next.js 15.5, the middleware now supports the Node.js runtime, which is required for the CrowdSec bouncer to function properly. You will need Next.js version 15.5 or higher to use this implementation. The middleware configuration includes `runtime: 'nodejs'` to enable this feature. For compatibility reasons, we still use custom API routes (`/api/crowdsec/remediation` and `/api/crowdsec/captcha`) to handle the bouncer logic separately from the middleware.
3133

32-
**Additional Note**: We had to update the Next.js configuration (`next.config.ts`) to copy font files from the `svg-captcha-fixed` library to make them available at runtime. This is necessary because Next.js doesn't automatically include these assets in the build, and the captcha functionality requires access to these fonts to generate captcha images.
34+
**Additional Notes**:
35+
- The Next.js configuration (`next.config.ts`) includes a custom Webpack plugin to copy font files from the `svg-captcha-fixed` library, making them available at runtime for captcha generation.
36+
- The project now includes Tailwind CSS v4 for styling the captcha page and other UI components.
37+
- Environment variables are loaded from `.env` files in the `nextjs` directory using `dotenv` and `dotenv-safe` for validation.
3338

3439
### Middleware (`src/middleware.ts`)
3540

3641
The middleware intercepts all requests and calls the CrowdSec API:
3742

3843
```js
3944
export async function middleware(req: NextRequest) {
40-
// Skip CrowdSec check for captcha route and non-HTML requests
41-
if (pathname === '/crowdsec-captcha' || !acceptHeader.includes('text/html')) {
42-
return NextResponse.next();
43-
}
44-
45-
// Call internal API to check IP remediation
46-
const checkUrl = `${req.nextUrl.origin}/api/crowdsec`;
47-
const res = await fetch(checkUrl, { method: 'POST' });
45+
// Check CrowdSec remediation using helper function
46+
const res = await checkRequestRemediation(req);
4847

49-
if (res.status !== 200) {
50-
// Return ban/captcha wall HTML
51-
const html = await res.text();
52-
return new NextResponse(html, {
53-
status: res.status,
54-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
55-
});
48+
if (res) {
49+
// Return ban/captcha wall if remediation is required
50+
return res;
5651
}
5752

5853
return NextResponse.next();
5954
}
55+
56+
export const config = {
57+
matcher: [
58+
// Match all routes except static files, APIs, and captcha page
59+
'/((?!api|_next/static|_next/image|fonts/|favicon.ico|robots.txt|sitemap.*\.xml|opengraph-image|captcha|ban).*)'
60+
],
61+
runtime: 'nodejs'
62+
}
6063
```
6164

62-
### API Route (`src/app/api/crowdsec/route.ts`)
65+
### API Routes
66+
67+
#### Remediation Check (`src/app/api/crowdsec/remediation/route.ts`)
6368

64-
The API route handles the CrowdSec logic:
69+
The remediation API route checks IP addresses and returns appropriate responses:
6570

6671
```js
67-
export async function POST(req: Request) {
72+
export async function GET(req: Request) {
73+
const ip = getIpFromRequest(req);
6874
const { remediation, origin } = await bouncer.getIpRemediation(ip);
6975
const bouncerResponse = await bouncer.getResponse({ ip, origin, remediation });
7076

@@ -78,15 +84,17 @@ export async function POST(req: Request) {
7884
}
7985
```
8086

81-
### Captcha Handler (`src/app/crowdsec-captcha/route.ts`)
87+
#### Captcha Handler (`src/app/api/crowdsec/captcha/route.ts`)
8288

83-
Handles captcha form submissions:
89+
Handles both captcha display and form submissions:
8490

8591
```js
92+
// Handle captcha form submission
8693
export async function POST(req: Request) {
8794
const form = await req.formData();
8895
const phrase = form.get('phrase')?.toString() || '';
8996
const refresh = form.get('crowdsec_captcha_refresh')?.toString() || '0';
97+
const ip = getIpFromRequest(req);
9098

9199
await bouncer.handleCaptchaSubmission({ ip, userPhrase: phrase, refresh, origin });
92100
return NextResponse.redirect(new URL('/', req.url));
@@ -101,18 +109,20 @@ export async function POST(req: Request) {
101109
102110
- You can run `nvm use` from the root folder to use the recommended NodeJS version for this project
103111
104-
- Copy the `.env.example` file to `.env` and fill in the required values
112+
- Copy the `.env.example` file to `.env` in the `nextjs` folder and fill in the required values
105113
106114
- Copy the `crowdsec/.env.example` file to `crowdsec/.env` and fill in the required values
107115
108-
- Install all dependencies using a local archive.
116+
- Install all dependencies.
109117
110-
Run the following commands from the `nextjs` folder:
118+
Run the following command from the `nextjs` folder:
111119
112120
```shell
113-
npm run pack-locally && npm install
121+
npm install
114122
```
115123
124+
**Note**: The `npm run dev` and `npm run start` commands will automatically build and pack the bouncer library before starting the server.
125+
116126
### Prepare the tests
117127
118128
1. Launch the docker instance:

examples/nextjs/crowdsec/.env.example

Lines changed: 0 additions & 2 deletions
This file was deleted.

examples/nextjs/docs/bypass.png

2.43 KB
Loading

examples/nextjs/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,22 @@
1111
},
1212
"dependencies": {
1313
"@crowdsec/nodejs-bouncer": "file:./cs-nodejs-bouncer.tgz",
14-
"dotenv": "^17.2.0",
14+
"dotenv": "^17.2.1",
1515
"dotenv-safe": "^9.1.0",
16-
"next": "^15.4.2",
17-
"pino": "^9.4.0",
18-
"react": "19.1.0",
19-
"react-dom": "19.1.0"
16+
"next": "^15.5.2",
17+
"pino": "^9.9.0",
18+
"react": "19.1.1",
19+
"react-dom": "19.1.1"
2020
},
2121
"devDependencies": {
2222
"@eslint/eslintrc": "^3",
2323
"@types/dotenv-safe": "^8.1.6",
24-
"@types/node": "^20",
24+
"@types/node": "^22",
2525
"@types/react": "^19",
2626
"@types/react-dom": "^19",
27-
"copy-webpack-plugin": "^13.0.0",
27+
"copy-webpack-plugin": "^13.0.1",
2828
"eslint": "^9",
29-
"eslint-config-next": "^15.4.1",
29+
"eslint-config-next": "^15.5.2",
3030
"typescript": "^5"
3131
}
3232
}

examples/nextjs/postcss.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const config = {
2+
plugins: {
3+
'@tailwindcss/postcss': {},
4+
},
5+
};
6+
export default config;

examples/nextjs/src/app/crowdsec-captcha/route.ts renamed to examples/nextjs/src/app/api/crowdsec/captcha/route.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
1-
import { NextResponse } from 'next/server';
2-
import { CrowdSecBouncer, CrowdSecBouncerConfigurations } from '@crowdsec/nodejs-bouncer';
3-
import { loadEnv } from '@/app/api/crowdsec/helpers';
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getCrowdSecBouncer } from '@/helpers/crowdsec';
3+
import { getIpFromRequest } from '@/helpers/ip';
44

5-
// Load and validate environment variables
6-
loadEnv();
5+
export async function POST(req: NextRequest) {
6+
const bouncer = await getCrowdSecBouncer();
77

8-
const config: CrowdSecBouncerConfigurations = {
9-
url: process.env.LAPI_URL ?? 'http://localhost:8080',
10-
bouncerApiToken: process.env.BOUNCER_KEY ?? '',
11-
wallsOptions: {
12-
captcha: {
13-
captchaAction: '/crowdsec-captcha',
14-
},
15-
},
16-
};
17-
18-
const bouncer = new CrowdSecBouncer(config);
19-
20-
export async function POST(req: Request) {
8+
const ip = await getIpFromRequest(req);
219
const form = await req.formData();
22-
const ip = process.env.BOUNCED_IP!;
2310
const phrase = form.get('phrase')?.toString() || '';
2411
const refresh = (form.get('crowdsec_captcha_refresh')?.toString() as '1') || '0';
2512

examples/nextjs/src/app/api/crowdsec/helpers/index.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

examples/nextjs/src/app/api/crowdsec/route.ts renamed to examples/nextjs/src/app/api/crowdsec/remediation/[ip]/route.ts

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,20 @@
11
'use server';
22

3-
import { CrowdSecBouncer, CrowdSecBouncerConfigurations } from '@crowdsec/nodejs-bouncer';
3+
import { getLogger } from '@/helpers';
4+
import { getCrowdSecBouncer } from '@/helpers/crowdsec';
45
import { NextResponse } from 'next/server';
56

6-
import { loadEnv, getLogger } from './helpers';
7-
8-
// Load and validate environment variables
9-
loadEnv();
10-
11-
const config: CrowdSecBouncerConfigurations = {
12-
url: process.env.LAPI_URL ?? 'http://localhost:8080',
13-
bouncerApiToken: process.env.BOUNCER_KEY ?? '',
14-
wallsOptions: {
15-
captcha: {
16-
captchaAction: '/crowdsec-captcha',
17-
},
18-
},
19-
};
20-
21-
const bouncer = new CrowdSecBouncer(config);
22-
237
// Set up the logger for this example
248
const logger = getLogger();
259

26-
export async function POST(req: Request) {
27-
const ip = process.env.BOUNCED_IP as string; // In a production scenario, the user's real IP should be retrieved.
10+
export async function GET(_req: Request) {
11+
const bouncer = await getCrowdSecBouncer();
12+
const ip = _req.url.split('/').pop(); // Get the IP from the request URL
13+
14+
if (!ip) {
15+
logger.warn('CrowdSec API route called without IP');
16+
return new NextResponse('IP not found', { status: 400 });
17+
}
2818

2919
logger.info(`CrowdSec API route called with IP: ${ip}`);
3020

0 commit comments

Comments
 (0)