From 61b1edcdd9fa9cd0cba7bde97d1cbd8a31ab12de Mon Sep 17 00:00:00 2001 From: giancarlo Date: Mon, 22 Apr 2024 11:56:42 +0800 Subject: [PATCH] Add useCsrfToken hook and update README A new `useCsrfToken` hook has been added for fetching CSRF token from the meta tag. The shared package's exports have been updated to include this new hook. Additionally, extensive documentation on writing server actions and API route handlers, as well as the usage of Captcha token protection and CSRF token, has been added to the README file. --- README.md | 237 ++++++++++++++++++++ packages/shared/package.json | 3 +- packages/shared/src/hooks/index.ts | 1 + packages/shared/src/hooks/use-csrf-token.ts | 13 ++ 4 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 packages/shared/src/hooks/index.ts create mode 100644 packages/shared/src/hooks/use-csrf-token.ts diff --git a/README.md b/README.md index 0ab8ae34f..311e3416c 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,241 @@ npx shadcn-ui@latest add button --path=packages/src/ui/shadcn We pass the `--path` flag to specify the path where the component should be installed. You may need to adjust the path based on your project structure. +**NB**: you may need to update the imports to the `cn` utility function to use the relative imports because it somehow breaks. Please do that. + +## Writing Server Actions + +In the large majority of cases - you will be writing React Server Actions to update data in your DB. + +Makerkit ships with a utility to help you write these actions. The utility is called `enhanceAction` and we import it from `@kit/next/actions`. + +```tsx +import { enhanceAction } from '@kit/next/actions'; +``` + +This utility helps us with three main things: +1. checks the user state (if the user is authenticated) +2. given a Zod schema, it validates the request body +3. given a captcha site key, it validates the captcha token + +Fantastic, let's see how we can use it. + +```tsx +'use server'; + +import { z } from 'zod'; +import { enhanceAction } from '@kit/next/actions'; + +const ZodSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), +}); + +export const myServerAction = enhanceAction( + async function (data, user) { + // 1. "data" has been validated against the Zod schema, and it's safe to use + // 2. "user" is the authenticated user + + // ... your code here + return { + success: true, + }; + }, + { + schema: ZodSchema, + }, +); +``` + +### Using a Captcha token protection + +If you want to protect your server actions with a captcha token, you can do so by passing the captcha site token to the `enhanceAction` function and setting the `captcha` flag to `true`. + +```tsx +'use server'; + +import { enhanceAction } from '@kit/next/actions'; + +export const myServerAction = enhanceAction( + async function (data, user) { + // ... your code here + return { + success: true, + }; + }, + { + captcha: true, + schema: ZodSchema, + }, +); +``` + +When calling the server action, we must supply the captcha token in the request body. + +The captcha token can be retrieved from the `useCaptchaToken` hook in the package `@kit/auth/captcha/client`. + +```tsx +import { useCaptchaToken } from '@kit/auth/captcha/client'; + +function Component() { + const captchaToken = useCaptchaToken(); + + // ... your code here +} +``` + +Now, when calling the server action, we can pass the captcha + +```tsx +import { useCaptchaToken } from '@kit/auth/captcha/client'; + +function Component() { + const captchaToken = useCaptchaToken(); + + const onSubmit = async (params: { + email: string; + password: string; + }) => { + const response = await myServerAction({ + ...params, + captchaToken, + }); + + // ... your code here + }; +} +``` + +NB: to use Captcha protection, you need to set the captcha token in the environment variables. + +```bash +CAPTCHA_SECRET_TOKEN= +``` + +As a secret environment variable, please do not add it to the `.env` file. Instead, add it to the environment variables of your CI/CD system. + +The only captcha provider supported is Cloudflare Turnstile. + +## Writing API Route Handlers + +You won't be writing too many API route handlers - but when you do, you can use the `enhanceRouteHandler` utility to help you with the following: + +1. checks the user state (if the user is authenticated) +2. given a Zod schema, it validates the request body +3. given a captcha site key, it validates the captcha token + +Fantastic, let's see how we can use it. + +```tsx +'use server'; + +import { z } from 'zod'; + +import { enhanceRouteHandler } from '@kit/next/routes'; +import { NextResponse } from 'next/server'; + +const ZodSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), +}); + +export const POST = enhanceRouteHandler( + async function({ body, user, request }) { + // 1. "body" has been validated against the Zod schema, and it's safe to use + // 2. "user" is the authenticated user + // 3. "request" is the request object that contains the headers, query, etc. + + // ... your code here + return NextResponse.json({ + success: true, + }); + }, + { + schema: ZodSchema, + }, +); +``` + +### Using a Captcha token protection + +If you want to protect your API route handlers with a captcha token, you can do so by passing the captcha site token to the `enhanceRouteHandler` function and setting the `captcha` flag to `true`. + +```tsx +'use server'; + +import { enhanceRouteHandler } from '@kit/next/routes'; + +export const POST = enhanceRouteHandler( + async function({ body, user, request }) { + // ... your code here + return NextResponse.json({ + success: true, + }); + }, + { + captcha: true, + schema: ZodSchema, + }, +); +``` + +When calling the API route handler, we must supply the captcha token in the request body. + +The captcha token can be retrieved from the `useCaptchaToken` hook in the package `@kit/auth/captcha/client`. + +```tsx +import { useCaptchaToken } from '@kit/auth/captcha/client'; + +function Component() { + const captchaToken = useCaptchaToken(); + + // ... your code here +} +``` + +Now, when calling the API route handler, we can pass the captcha and the CSRF token. + +NB: The CSRF token **must be added for all API routes** making mutations in routes that are outside `/api/*`. Routes inside `/api/*` are not protected by default as they're meant to be used externally. + +```tsx +import { useCaptchaToken } from '@kit/auth/captcha/client'; +import { useCsrfToken } from '@kit/shared/hooks'; + +function Component() { + const captchaToken = useCaptchaToken(); + const csrfToken = useCsrfToken(); + + const onSubmit = async (params: { + email: string; + password: string; + }) => { + const response = await fetch('/api/my-api-route', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-csrf-token': csrfToken, + }, + body: JSON.stringify({ + ...params, + captchaToken, + }), + }); + + // ... your code here + }; +} +``` + +NB: to use Captcha protection, you need to set the captcha token in the environment variables. + +```bash +CAPTCHA_SECRET_TOKEN= +``` + +As a secret environment variable, please do not add it to the `.env` file. Instead, add it to the environment variables of your CI/CD system. + +The only captcha provider supported is Cloudflare Turnstile. + ## Environment Variables The majority of the environment variables are defined in the `apps/web/.env` file. These are the env variables @@ -420,6 +655,8 @@ As the endpoint, remember to use the `/api/db/webhook` endpoint. If your APP url #### Adding Database Webhooks from Supabase Studio +The below is only needed when going to production. The local development seed.sql script will add the webhooks for you. + While you can create a migration to add the database webhooks, you can also add them from the Supabase Studio. 1. Go to the Supabase Studio diff --git a/packages/shared/package.json b/packages/shared/package.json index 90e72b167..ecd0f7304 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -11,7 +11,8 @@ "prettier": "@kit/prettier-config", "exports": { "./logger": "./src/logger/index.ts", - "./utils": "./src/utils.ts" + "./utils": "./src/utils.ts", + "./hooks": "./src/hooks/index.ts" }, "dependencies": { "@tanstack/react-table": "^8.16.0", diff --git a/packages/shared/src/hooks/index.ts b/packages/shared/src/hooks/index.ts new file mode 100644 index 000000000..f132daf62 --- /dev/null +++ b/packages/shared/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-csrf-token'; diff --git a/packages/shared/src/hooks/use-csrf-token.ts b/packages/shared/src/hooks/use-csrf-token.ts new file mode 100644 index 000000000..c852d16f0 --- /dev/null +++ b/packages/shared/src/hooks/use-csrf-token.ts @@ -0,0 +1,13 @@ +/** + * Get the CSRF token from the meta tag. + * @returns The CSRF token. + */ +export function useCsrfToken() { + const meta = document.querySelector('meta[name="csrf-token"]'); + + if (!meta) { + return ''; + } + + return meta.getAttribute('content') ?? ''; +}