Company
Date Published
Author
Paul Bratslavsky
Word count
6261
Language
English
Hacker News points
None

Summary

In this tutorial, we will build a Sign Up and Sign In page for a Next.js application using Strapi as our backend API. We will use server actions to handle form submissions and integrate with the backend API. Additionally, we will cover setting up httpOnly cookies for secure authentication and protecting routes through Next.js middleware. First, let's create a new Next.js project: ```bash npx create-next-app@latest --use-npm ``` Next, install the necessary dependencies: ```bash npm install @strapi/react react-router-dom zod zod-form-data axios ``` Now, let's create a new file called `utils.js` in the root directory of our project and add the following code to it: ```javascript // utils.js export const getStrapiURL = () => { return process.env.STRAPI_URL || "http://localhost:1337"; }; export const getAuthToken = async () => { const cookieStore = await cookies(); const authToken = cookieStore.get("jwt")?.value; return authToken; }; ``` Now, let's create a new file called `auth-actions.js` in the root directory of our project and add the following code to it: ```javascript // auth-actions.js import { z } from "zod"; import axios from "axios"; import { getStrapiURL, getAuthToken } from "./utils"; const schemaRegister = z.object({ username: z.string().min(3).max(20), email: z.string().email(), password: z.string().min(6).max(100), }); export async function registerUserAction(prevState, formData) { const validatedFields = schemaRegister.safeParse(formData); if (!validatedFields.success) { return { ...prevState, zodErrors: validatedFields.error.flatten() }; } try { const response = await axios.post(`${getStrapiURL()}/auth/local/register`, { username: validatedFields.data.username, email: validatedFields.data.email, password: validatedFields.data.password, }); if (response.status === 200) { return { ...prevState, message: "User registered successfully" }; } else { throw new Error("Failed to register user"); } } catch (error) { console.error(error); return { ...prevState, strapiErrors: error.message }; } } const schemaLogin = z.object({ identifier: z.string().min(3).max(20), password: z.string().min(6).max(100), }); export async function loginUserAction(prevState, formData) { const validatedFields = schemaLogin.safeParse(formData); if (!validatedFields.success) { return { ...prevState, zodErrors: validatedFields.error.flatten() }; } try { const response = await axios.post(`${getStrapiURL()}/auth/local`, { identifier: validatedFields.data.identifier, password: validatedFields.data.password, }); if (response.status === 200) { const authToken = response.data.jwt; setCookie("jwt", authToken, { ...config, maxAge: 60 * 60 * 24 * 7 }); return { ...prevState, message: "Logged in successfully" }; } else { throw new Error("Failed to log in"); } } catch (error) { console.error(error); return { ...prevState, strapiErrors: error.message }; } } export async function logoutAction() { try { const authToken = await getAuthToken(); if (!authToken) { throw new Error("No token found"); } const response = await axios.post(`${getStrapiURL()}/users/logout`, null, { headers: { Authorization: `Bearer ${authToken}` }, }); if (response.status === 200) { removeCookie("jwt"); return { ...prevState, message: "Logged out successfully" }; } else { throw new Error("Failed to logout"); } } catch (error) { console.error(error); return { ...prevState, strapiErrors: error.message }; } } ``` Now, let's create a new file called `signup-form.js` in the root directory of our project and add the following code to it: ```javascript // signup-form.js import { useActionState } from "react"; import { schemaRegister } from "./auth-actions"; import { Input, Button, FormControl, Text } from "@chakra-ui/react"; import { ZodErrors } from "../components/custom/zod-errors"; const INITIAL_STATE = { zodErrors: null, strapiErrors: null, data: null, message: null, }; export function SignupForm() { const [formState, formAction] = useActionState(registerUserAction, INITIAL_STATE); return ( <div> <Text fontSize="2xl" mb={4}>Sign Up</Text> <form action={formAction}> <FormControl isInvalid={!!formState.zodErrors?.username}> <Input id="username" name="username" type="text" placeholder="Username" value={formState.data?.username || ""} onChange={(e) => formAction({ ...formState, data: { ...formState.data, username: e.target.value } })} /> <ZodErrors error={formState.zodErrors?.username} /> </FormControl> <FormControl isInvalid={!!formState.zodErrors?.email}> <Input id="email" name="email" type="email" placeholder="Email" value={formState.data?.email || ""} onChange={(e) => formAction({ ...formState, data: { ...formState.data, email: e.target.value } })} /> <ZodErrors error={formState.zodErrors?.email} /> </FormControl> <FormControl isInvalid={!!formState.zodErrors?.password}> <Input id="password" name="password" type="password" 78910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112121312141215121612171218121912212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121213121412151216121712181219122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200