Skip to content

Commit 57ee827

Browse files
committed
feat: Add captcha
1 parent bcf7bb8 commit 57ee827

File tree

23 files changed

+576
-48
lines changed

23 files changed

+576
-48
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
---
2+
title: Captcha
3+
description: Protect your forms and API call with captcha validation.
4+
---
5+
6+
## Usage
7+
8+
In this example, we will show you how to use captcha in your forms. We will use the `AutoForm` component to render the form and handle the captcha validation.
9+
10+
import { Step, Steps } from 'fumadocs-ui/components/steps';
11+
12+
<Steps>
13+
<Step>
14+
15+
### Activate captcha in route
16+
17+
Add `withCaptcha` to your route config to enable captcha validation for this route.
18+
19+
```ts title="plugins/{plugin_name}/src/routes/example.ts"
20+
import { buildRoute } from '@vitnode/core/api/lib/route';
21+
22+
export const exampleRoute = buildRoute({
23+
...CONFIG_PLUGIN,
24+
route: {
25+
method: 'post',
26+
description: 'Create a new user',
27+
path: '/sign_up',
28+
withCaptcha: true, // [!code ++]
29+
},
30+
handler: async c => {},
31+
});
32+
```
33+
34+
</Step>
35+
<Step>
36+
37+
### Get config from middleware API
38+
39+
Get captcha config from middleware API in your view and pass it to your `'use client';` component.
40+
41+
```tsx title="plugins/{plugin_name}/src/app/sing_up/page.tsx"
42+
import { getMiddlewareApi } from '@vitnode/core/lib/api/get-middleware-api'; // [!code ++]
43+
44+
export const SignUpView = async () => {
45+
const { captcha } = await getMiddlewareApi(); // [!code ++]
46+
47+
return <FormSignUp captcha={captcha} />;
48+
};
49+
```
50+
51+
</Step>
52+
<Step>
53+
54+
### Use in form
55+
56+
Get the `captcha` config from the props and pass it to the `AutoForm` component. This will render the captcha widget in your form.
57+
58+
```tsx title="plugins/{plugin_name}/src/components/form/sign-up/sign-up.tsx"
59+
'use client';
60+
61+
import { AutoForm } from '@vitnode/core/components/form/auto-form';
62+
63+
export const FormSignUp = ({
64+
captcha, // [!code ++]
65+
}: {
66+
captcha: z.infer<typeof routeMiddlewareSchema>['captcha']; // [!code ++]
67+
}) => {
68+
return (
69+
<AutoForm<typeof formSchema>
70+
captcha={captcha} // [!code ++]
71+
fields={[]}
72+
formSchema={formSchema}
73+
/>
74+
);
75+
};
76+
```
77+
78+
<Card
79+
title="AutoForm"
80+
description="Lear more about the AutoForm component and how to use it."
81+
href="/docs/ui/auto-form"
82+
/>
83+
84+
</Step>
85+
<Step>
86+
87+
### Submit form with captcha
88+
89+
In your form submission handler, you can get the `captchaToken` from the form submission context and pass it to your mutation API.
90+
91+
```tsx title="plugins/{plugin_name}/src/components/form/sign-up/sign-up.tsx"
92+
'use client';
93+
94+
import {
95+
AutoForm,
96+
type AutoFormOnSubmit, // [!code ++]
97+
} from '@vitnode/core/components/form/auto-form';
98+
99+
export const FormSignUp = ({
100+
captcha,
101+
}: {
102+
captcha: z.infer<typeof routeMiddlewareSchema>['captcha'];
103+
}) => {
104+
const onSubmit: AutoFormOnSubmit<typeof formSchema> = async (
105+
values,
106+
form,
107+
{ captchaToken }, // [!code ++]
108+
) => {
109+
// Call your mutation API with captcha token
110+
await mutationApi({
111+
...values,
112+
captchaToken, // [!code ++]
113+
});
114+
115+
// Handle success or error
116+
};
117+
118+
return (
119+
<AutoForm<typeof formSchema>
120+
captcha={captcha}
121+
fields={[]}
122+
onSubmit={onSubmit} // [!code ++]
123+
formSchema={formSchema}
124+
/>
125+
);
126+
};
127+
```
128+
129+
Next, you need to set `captchaToken` in your mutation API call. This token is provided by the `AutoForm` component when the form is submitted.
130+
131+
```tsx title="plugins/{plugin_name}/src/components/form/sign-up/mutation-api.ts"
132+
'use server';
133+
134+
import type { z } from 'zod';
135+
136+
import { fetcher } from '@vitnode/core/lib/fetcher';
137+
138+
export const mutationApi = async ({
139+
captchaToken, // [!code ++]
140+
...input
141+
// [!code ++]
142+
}: z.infer<typeof zodSignUpSchema> & { captchaToken }) => {
143+
const res = await fetcher(usersModule, {
144+
path: '/sign_up',
145+
method: 'post',
146+
module: 'users',
147+
captchaToken, // [!code ++]
148+
args: {
149+
body: input,
150+
},
151+
});
152+
153+
if (res.status !== 201) {
154+
return { error: await res.text() };
155+
}
156+
157+
const data = await res.json();
158+
159+
return { data };
160+
};
161+
```
162+
163+
</Step>
164+
</Steps>
165+
166+
## Custom Usage
167+
168+
If you want to use captcha in your custom form or somewhere else, follow these steps.
169+
170+
<Steps>
171+
<Step>
172+
173+
### Activate captcha in route
174+
175+
```ts title="plugins/{plugin_name}/src/routes/example.ts"
176+
import { buildRoute } from '@vitnode/core/api/lib/route';
177+
178+
export const exampleRoute = buildRoute({
179+
...CONFIG_PLUGIN,
180+
route: {
181+
method: 'post',
182+
description: 'Create a new user',
183+
path: '/sign_up',
184+
withCaptcha: true, // [!code ++]
185+
},
186+
handler: async c => {},
187+
});
188+
```
189+
190+
</Step>
191+
<Step>
192+
193+
### Get config from middleware API
194+
195+
```tsx title="plugins/{plugin_name}/src/app/sing_up/page.tsx"
196+
import { getMiddlewareApi } from '@vitnode/core/lib/api/get-middleware-api'; // [!code ++]
197+
198+
export const SignUpView = async () => {
199+
const { captcha } = await getMiddlewareApi(); // [!code ++]
200+
201+
return <FormSignUp captcha={captcha} />;
202+
};
203+
```
204+
205+
</Step>
206+
<Step>
207+
208+
### Use `useCaptcha` hook
209+
210+
Inside your client component, use the `useCaptcha` hook to handle captcha rendering and validation. Remember to add `div` with `id="vitnode_captcha"` where you want the captcha widget to appear.
211+
212+
```tsx title="plugins/{plugin_name}/src/components/form/sign-up/sign-up.tsx"
213+
'use client';
214+
215+
import { AutoForm } from '@vitnode/core/components/form/auto-form';
216+
217+
export const FormSignUp = ({
218+
captcha, // [!code ++]
219+
}: {
220+
captcha: z.infer<typeof routeMiddlewareSchema>['captcha']; // [!code ++]
221+
}) => {
222+
// [!code ++]
223+
const { isReady, token, onReset } = useCaptcha(captcha);
224+
225+
const onSubmit = async () => {
226+
await mutationApi({
227+
// ...other values,
228+
captchaToken: token, // [!code ++]
229+
});
230+
231+
// Handle success or error
232+
// [!code ++]
233+
onReset(); // Reset captcha after submission
234+
};
235+
236+
return (
237+
<form onSubmit={onSubmit}>
238+
{/* Render captcha widget */}
239+
{/* [!code ++] */}
240+
<div id="vitnode_captcha" />
241+
242+
<Button disabled={!isReady}>Submit</Button>
243+
</form>
244+
);
245+
};
246+
```
247+
248+
</Step>
249+
<Step>
250+
251+
### Submit form with captcha
252+
253+
```tsx title="plugins/{plugin_name}/src/components/form/sign-up/mutation-api.ts"
254+
'use server';
255+
256+
import type { z } from 'zod';
257+
258+
import { fetcher } from '@vitnode/core/lib/fetcher';
259+
260+
export const mutationApi = async ({
261+
captchaToken, // [!code ++]
262+
}: {
263+
// [!code ++]
264+
captchaToken;
265+
}) => {
266+
await fetcher(usersModule, {
267+
path: '/test',
268+
method: 'post',
269+
module: 'blog',
270+
captchaToken, // [!code ++]
271+
});
272+
};
273+
```
274+
275+
</Step>
276+
</Steps>

apps/docs/content/docs/dev/index.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ npx create-vitnode-app@canary
2727
```
2828

2929
</Tabs>
30+
31+
## Why VitNode?
32+
33+
something here

apps/docs/content/docs/dev/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"---Framework---",
2222
"config",
2323
"logging",
24+
"captcha",
2425
"---Advanced---",
2526
"..."
2627
]

apps/docs/content/docs/ui/auto-form.mdx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,12 @@ The `onSubmit` callback provides access to the React Hook Form instance as a sec
131131

132132
You can also define the submission handler separately:
133133

134+
```ts
135+
import type { AutoFormOnSubmit } from '@vitnode/core/components/form/auto-form';
136+
```
137+
134138
```tsx
135-
const onSubmit = async (
136-
values: z.infer<typeof formSchema>,
137-
form: UseFormReturn<z.infer<typeof formSchema>>,
138-
) => {
139+
const onSubmit: AutoFormOnSubmit<typeof formSchema> = async (values, form) => {
139140
try {
140141
await saveData(values);
141142
toast.success('Form submitted successfully');

apps/web/src/vitnode.api.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export const POSTGRES_URL =
1717
process.env.POSTGRES_URL || 'postgresql://root:root@localhost:5432/vitnode';
1818

1919
export const vitNodeApiConfig = buildApiConfig({
20+
captcha: {
21+
type: 'cloudflare_turnstile',
22+
siteKey: process.env.CLOUDFLARE_TURNSTILE_SITE_KEY,
23+
secretKey: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY,
24+
},
2025
plugins: [blogApiPlugin()],
2126
dbProvider: drizzle({
2227
connection: POSTGRES_URL,

packages/vitnode/src/api/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function VitNodeAPI({
6565
metadata: vitNodeConfig.metadata,
6666
authorization: vitNodeApiConfig.authorization,
6767
dbProvider: vitNodeApiConfig.dbProvider,
68+
captcha: vitNodeApiConfig.captcha,
6869
}),
6970
);
7071
app.use(async (c, next) => {

packages/vitnode/src/api/lib/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { RouteConfig, RouteHandler } from '@hono/zod-openapi';
22

33
import { createRoute as createRouteHono } from '@hono/zod-openapi';
44

5+
import { captchaMiddleware } from '../middlewares/captcha.middleware';
56
import {
67
type EnvVitNode,
78
pluginMiddleware,
@@ -21,6 +22,7 @@ export const buildRoute = <
2122
P extends string,
2223
R extends Omit<RouteConfig, 'path'> & {
2324
path: P;
25+
withCaptcha?: boolean;
2426
},
2527
H extends ValidHandler<R & { path: P }>,
2628
>({
@@ -50,6 +52,7 @@ export const buildRoute = <
5052
tags,
5153
middleware: [
5254
pluginMiddleware(pluginId),
55+
...(route.withCaptcha ? [captchaMiddleware()] : []),
5356
...(Array.isArray(route.middleware)
5457
? route.middleware
5558
: route.middleware

0 commit comments

Comments
 (0)