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') ?? ''; +}