From 926b588b6b9baaf0e8fc88fb153916e2fa88ef9a Mon Sep 17 00:00:00 2001
From: Esther Lizardo <100808713+Esther-Lita@users.noreply.github.com>
Date: Tue, 12 Sep 2023 16:44:40 +0200
Subject: [PATCH 01/23] Create Nextjs.mdx
---
docs/docs/guides/tutorials/Nextjs.mdx | 752 ++++++++++++++++++++++++++
1 file changed, 752 insertions(+)
create mode 100644 docs/docs/guides/tutorials/Nextjs.mdx
diff --git a/docs/docs/guides/tutorials/Nextjs.mdx b/docs/docs/guides/tutorials/Nextjs.mdx
new file mode 100644
index 000000000..eed699d40
--- /dev/null
+++ b/docs/docs/guides/tutorials/Nextjs.mdx
@@ -0,0 +1,752 @@
+# Using Hanko for a Todo app with Next.js 13 and Prisma
+
+In this tutorial, you’ll learn how to build a Todo app with the Next.js 13 popular “App Router” structure and understand some of the most important changes that come with it. We will build a fully functional todo app, handling the creation, updating when completed and the option to delete the todos.
+
+## Let’s set it up
+At Hanko, we understand that finding the right stack for the project is a big step. In this guide, we will bring Next.js as the main character of the project, we will test the Client vs Server Components. For the style, we will use Tailwind CSS. We will use Hanko for the login and registration, user management and logout. Prisma will handle the storage.
+
+### Initiliaze the next app
+To create a new Next.js app, we can use the `create-next-app` or `create-next-app@latest` command-line tool followed by the name of your choice for the app. Open your terminal in Visual Studio Code and run the following command:
+
+```
+ npx create-next-app@latest todo-nextjs-hanko
+```
+
+Then you’ll be asked some prompts on what you will use for the app. The project configuration options should look something like this:
+
+![Pre-config Next.js prompts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8djxl7jl42xl90wpps1b.png)
+
+The above choices will create a new Next.js app with the chosen name, all the required dependencies for this project will also be installed.
+
+#### Understanding the project structure
+When using the version 13 of Next.js, we have the option to work with the App Router directory instead of the Pages Router, for a quick summary we could say that:
+
+* The new directory named “app” is replacing “pages”
+* “page.tsx|page.jsx” is the new “index.tsx|index.jsx”
+* “layout.tsx” is the new “_app.tsx”
+* Everything is a Server Component unless you make it a Client Component using the “use client” directive at the top of the file.
+* API Routes are now Server Components or Route Handlers (... More info on how this is very important)
+
+Remove unnecessary files, such as logos, icons, etc. If you are going to use Tailwind CSS make sure to bring your desired configuration to the `tailwind.config.ts` file, defining your color palette, fonts, breakpoints, ect.
+
+ℹ️ For more information about the App Router of Next.js click [here](https://nextjs.org/docs).
+
+### Get Prisma started
+Install the Prisma CLI as a development dependency in the project:
+
+```
+$ npm install prisma --save-dev
+```
+
+Set up Prisma with the init command of the Prisma CLI:
+
+```
+$ npx prisma init --datasource-provider sqlite
+
+```
+This creates a new `prisma` directory with your Prisma schema file and configures SQLite as your database. Once we also create the "Todo" model, the Prisma schema file should look like this:
+
+```
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+model Todo {
+
+ id String @id @default(uuid())
+ title String
+ complete Boolean
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+```
+At this point, you have a Prisma schema but no database yet. Run the following command in your terminal to create the SQLite database and the Todo table:
+
+```
+ $ npx prisma migrate dev --name init
+```
+This command did two things:
+
+1. It creates a new SQL migration file for this migration in the `prisma/migrations` directory.
+2. It runs the SQL migration file against the database.
+
+Because the SQLite database file didn't exist before, the command also created it inside the `prisma` directory with the name `dev.db` as defined via the environment variable in the `.env` file.
+
+To prevent problems when instantiating PrismaClient, on the Prisma Docs there’s a [section](https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices) dedicated to the best practice to do it. Let’s try it by creating a `db.ts` file in the root of the app and add the following code inside:
+
+```javascript
+import { PrismaClient } from "@prisma/client";
+
+const globalForPrisma = globalThis as unknown as {
+ prisma: PrismaClient | undefined;
+};
+
+export const prisma = globalForPrisma.prisma ?? new PrismaClient();
+
+if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
+
+```
+ℹ️ For more information about Prisma integration click [here](https://www.prisma.io/docs/getting-started).
+
+
+
+## Building the user interface
+The goal is to build a simple "todo app" with a nice login to protect the todos, for this we will only need two pages:
+* The login page where the Hanko-auth component will play its part in handling authentication.
+* The todo page where all the todos will be displayed.
+
+### App structure
+In the App Router directory, the `page.tsx` is like the new `index.tsx`, which means that this name will play an important role when creating a new route. You can define a page by exporting a component from a `page.tsx` file.
+
+Now you can update the `page.tsx` file to display "Hello World" as done below.
+
+```javascript
+ export default function Home() {
+return (
+
+
Hello World
+
+);
+}
+```
+
+ We will get back to it later to add a nice login with Hanko.
+
+### The Todo page
+We will style this page using Tailwind CSS classes to create a centered container to display the todos. we need a form with an input to create the new todos, and every todo element will have a checkbox and a delete button. Inside the `app` directory, create a new `todo` folder with a `page.tsx` file inside of it. Use the code below as the `todo/page.tsx` contents:
+
+```javascript
+export default function Todo() {
+ return (
+
+
+
My to dos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+```
+
+ℹ️ For a better understanding of the Tailwind CSS classes click [here](https://tailwindcomponents.com/cheatsheet/).
+
+
+## Todos in the making
+To make our app functional, we need to be able to create a new todo, then check the todo once it’s completed and finally be able to remove a single todo from the list.
+
+### API Routes in Next.js 13
+What we know as API Routes are replaced by Route Handlers and they are defined in a `route.ts|js` file inside the `app` directory. Read more about the Route Handlers in the [Next.js Docs](https://nextjs.org/docs/app/building-your-application/routing/route-handlers).
+
+Inside the `app` directory create an `api` folder. We will group our Route Handlers as follows: one directory `todo` with a `route.tsx` which will contain the `POST` HTTP method handler for creating a new todo, and in that same directory we will use a dynamic route to `GET` and `DELETE` todos. Should look like the following example:
+
+```
+api
+└── todo
+ ├── [id]
+ │ └── route.ts
+ └── route.ts
+```
+
+### New Todo
+This is a good moment to start breaking it down into components, let’s first create a `components` folder at the root directory, then create a `components/todos/NewTodo.tsx` file and use the following as its contents:
+
+```javascript
+"use client";
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+
+export const NewTodo = () => {
+ const [newItem, setNewItem] = useState("");
+
+ const router = useRouter();
+ const create = async (e: React.SyntheticEvent) => {
+ e.preventDefault();
+ await fetch(`/api/todo`, {
+ method: "POST",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ title: newItem,
+ }),
+ });
+
+ router.refresh();
+ setNewItem("");
+ };
+ return (
+
+
+
+ );
+}
+
+```
+
+This is a good example of where to use the "use client" directive since we are using `useState()` and subscribing to interactive events.
+
+This is how we call Prisma to create the todo inside the `api/todo/route.ts` Route Handler:
+
+```javascript
+import { NextResponse } from "next/server";
+import { prisma } from "@/db";
+
+export async function POST(req: Request) {
+ const { title } = await req.json();
+
+ await prisma.todo.create({
+ data: { title, complete: false },
+ });
+
+ return NextResponse.json({ message: "Created Todo" }, { status: 200 });
+}
+```
+
+Since everything on Next.js 13 runs in the server, we can now call Prisma from the `todo/page.tsx` file to get all our todos, then we pass them to our `components/todos/TodoItem.tsx` file to be displayed. This is how the `todo/page.tsx` should look after our changes:
+
+```javascript
+import { NewTodo } from "@/components/todos/NewTodo";
+import { TodoItem } from "@/components/todos/TodoItem";
+import { prisma } from "@/db";
+
+export default async function Todo() {
+ const todos = await prisma.todo.findMany();
+
+ return (
+
+
+
My to dos
+
+
+
+
+
+
+ );
+}
+```
+🚨 Client Components themselves cannot be async functions ([official FAQ](https://nextjs.org/docs/messages/no-async-client-component)). Prisma will break the app if you try to call it inside a Client Component.
+
+### Update and Delete todo by ID
+In the next step, we need a way to handle marking a todo as completed and to handle the deletion of a todo. Accordingly, we create `update` and `delete` functions that fetch our dynamic route. This would be the `components/todos/TodoItem.tsx` file:
+
+```javascript
+"use client";
+import { useRouter } from "next/navigation";
+import { Todo } from "@prisma/client";
+
+export const TodoItem = ({ todos }: { todos: Todo[] }) => {
+ const router = useRouter();
+ const update = async (todo: Todo) => {
+ await fetch(`/api/todo/${todo.id}`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ completed: !todo.complete,
+ }),
+ });
+ router.refresh();
+ };
+
+ const deleteTodo = async (todo: Todo) => {
+ await fetch(`/api/todo/${todo.id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ id: todo.id,
+ }),
+ });
+
+ router.refresh();
+ };
+ return (
+ <>
+ {todos.map((todo) => {
+ return (
+
+ );
+ })}
+ >
+ );
+};
+```
+
+Add the following code inside the `api/todo/[id]/route.tsx` Route Handler:
+
+```javascript
+import { NextResponse } from "next/server";
+import { prisma } from "@/db";
+
+export async function PATCH(
+ req: Request,
+ { params: { id } }: { params: { id: string } }
+) {
+ const { completed } = await req.json();
+
+ await prisma.todo.update({
+ where: {
+ id: id,
+ },
+ data: {
+ complete: completed,
+ },
+ });
+ return NextResponse.json({ message: "Updated" }, { status: 200 });
+}
+
+export async function DELETE(req: Request) {
+ const { id } = await req.json();
+
+ await prisma.todo.delete({
+ where: {
+ id: id,
+ },
+ });
+ return NextResponse.json({ message: "Deleted Item" }, { status: 200 });
+}
+```
+
+ℹ️ For more information on the Prisma Client Api click [here](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference).
+
+
+## Authentication by Hanko
+Now is time to start working on the security.
+
+### Hanko Cloud setup
+Visit [Hanko Cloud](https://cloud.hanko.io/login) and create an account. Then create an organization to manage your Hanko project.
+
+![Create a Hanko Organization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8vho8qsrbkkthn6602t.png)
+
+Then create a new project and set the App URL to your development URL (example: http://localhost:3000):
+
+![Hanko Cloud Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hgfioy9p3wet6t5fe7sn.png)
+
+And that’s all! Now you can always return to your Hanko Cloud dashboard to see your API URL and other insights about your project, you can also change the app URL in the settings, so that once you want to move from "development" to "production", you can change it to a proper domain/URL. Take the time to discover all the features.
+
+![Hanko Cloud dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvan097duorvng3c1my0.png)
+
+### Adding Hanko to the Next.js app
+Let’s bring Hanko to the game by installing the package running the code below:
+
+```
+npm install @teamhanko/hanko-elements
+```
+First, let’s update our "Home" page and rename the function to "Login". Import the register function from `@teamhanko/hanko-elements`, and call the function with the Hanko API URL as an argument to register the ``. Now include it in your JSX:
+
+```javascript
+"use client";
+import { useEffect} from "react";
+import { register } from "@teamhanko/hanko-elements";
+
+const hankoApi = "YOUR_HANKO_API_URL";
+export default function Login() {
+ useEffect(() => {
+ //
+ register(hankoApi ?? "").catch((error) => {
+ console.log(error);
+ });
+ }, []);
+
+ return (
+
+
+
+ );
+}
+```
+The code snippet above should display the Hanko authentication component:
+
+![Hanko Authentication component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ma10mk3z4sk7h925v03t.png)
+
+ The `` component offers a page for managing email addresses and passkeys. Let's create a profile button component by creating a file `components/Profile.tsx` and use the following code as its content:
+
+```javascript
+"use client";
+import { useEffect, useState } from "react";
+import { register } from "@teamhanko/hanko-elements";
+
+const hankoApi = "YOUR_HANKO_API_URL";
+
+export const Profile = () => {
+ const [openState, setOpenState] = useState(false);
+
+ useEffect(() => {
+ register(hankoApi ?? "").catch((error) => {
+ console.log(error);
+ });
+ }, []);
+
+ const openProfile = () => {
+ setOpenState(!openState);
+ };
+
+ return (
+ <>
+
+ {openState && (
+
+
+
+
+
+ )}
+ >
+ );
+};
+```
+It should look like this:
+
+![Hanko Profile Component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmp0lj2lfwdg5kdzlp5c.png)
+
+Now let’s use `@teamhanko/hanko-elements` to manage user logouts by creating a logout button component. Create a file `components/Logout.tsx` and use the following as its content:
+
+```javascript
+"use client";
+import { useState, useEffect, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import { Hanko } from "@teamhanko/hanko-elements";
+
+const hankoApi = "YOUR_HANKO_API_URL";
+
+export const Logout = () => {
+ const router = useRouter();
+ const [hanko, setHanko] = useState();
+
+ useEffect(() => {
+ import("@teamhanko/hanko-elements").then(({ Hanko }) =>
+ setHanko(new Hanko(hankoApi ?? ""))
+ );
+ }, []);
+
+ const logout = () => {
+ hanko?.user
+ .logout()
+ .then(() => {
+ router.push("/");
+ router.refresh();
+ return;
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+ };
+ return (
+ <>
+
+ >
+ );
+};
+```
+ When a user logs out, a specific event is triggered that you can subscribe to, like redirecting to a specific page after logout:
+
+```javascript
+ const renewSession = useCallback(() => {
+ router.replace("/");
+ }, [router]);
+
+ useEffect(
+ () =>
+ hanko?.onSessionExpired(() => {
+ renewSession();
+ }),
+
+ [hanko, renewSession]
+ );
+```
+ℹ️ For more information about all the events that you can "listen" from the Hanko client click [here](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#events).
+
+ℹ️ You can also find information on how to customize Hanko Components by clicking [here](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#ui-customization).
+
+
+## Verifying JWT with jose library
+The JWT is signed by Hanko and to secure our app we still need to verify the JWT.
+
+> **What are JWTs?**
+> _A JSON Web Token (JWT) is a compact and self-contained way for transmitting information between parties as a JSON object in a secure way. The purpose of a JWT is to ensure the authenticity of the data._
+
+Hanko handles the authentication and signing of the JWT, on successful authentication with Hanko a cookie, which contains said JWT as its value, is set. we don’t really need to know a lot about them but it’s worth getting familiar with the parts of a JWT (header, payload and signature), and what is a [JWKS](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets), for more information you can visit [JWT.io](https://jwt.io/).
+
+To verify the JWT we need to install the `jose-jwt` package:
+
+```
+npm i jose
+```
+Jose is a JavaScript module that supports JWT and provides functionality for signing and verifying tokens.
+
+ℹ️ For more information about Jose click [here](https://www.npmjs.com/package/jose).
+
+### Middleware
+Create a new file `middleware.tsx` in the root of your project and use the following code:
+
+```javascript
+import * as jose from "jose";
+import { NextRequest } from "next/server";
+
+const hankoApi = "YOUR_HANKO_API_URL";
+
+export default async function middleware(req: NextRequest) {
+ const token = req.cookies.get("hanko")?.value;
+
+ const JWKS = jose.createRemoteJWKSet(
+ new URL(`${hankoApi}/.well-known/jwks.json`)
+ );
+
+ try {
+ const verifiedJWT = await jose.jwtVerify(token, JWKS);
+ console.log(verifiedJWT);
+ } catch {
+return NextResponse.redirect(new URL("/", req.url));
+}
+}
+```
+To verify the JWT we need the token and the JWKS. We get the token from the "hanko" cookie, and then we obtain the JSON Web Key Set (JWKS) calling the `createRemoteJWKSet` function from jose. Then we call `await jose.jwtVerify(token, JWKS)`. If the token can be verified, then the promise returned from the function resolves to a decoded token. If it cannot be verified, then the promise rejects and we can catch the error and handle it appropriately, e.g. by redirecting the user to the login/home page. If you console.log the const `verifiedJWT` you should see the decoded token showing the payload, the protectedHeader and the key. Inside the key, you should be able to see a "true" if it’s verified.
+
+ℹ️ For more information about Next.js Middleware click [here](https://nextjs.org/docs/app/building-your-application/routing/middleware#conditional-statements).
+
+### Securing the application and redirecting
+To prevent unauthorized users from getting access to private user data, we can add the paths to be protected in the Middleware configuration. Copy the following code at the bottom of your `middleware.tsx` file:
+
+```javascript
+export const config = {
+ matcher: ["/todo"],
+};
+```
+
+Update the `Login` page to subscribe to the events of the Hanko client and redirect to the `Todo` page after a successful login:
+
+```javascript
+"use client";
+import { useEffect, useState, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import { register } from "@teamhanko/hanko-elements";
+
+const hankoApi = "YOUR_HANKO_API_URL";
+export default function Login() {
+ const router = useRouter();
+ const [hanko, setHanko] = useState();
+
+ useEffect(() => {
+ import("@teamhanko/hanko-elements").then(({ Hanko }) =>
+ setHanko(new Hanko(hankoApi ?? ""))
+ );
+ }, []);
+
+ const redirectAfterLogin = useCallback(() => {
+ router.replace("/todo");
+ }, [router]);
+
+ useEffect(
+ () =>
+ hanko?.onAuthFlowCompleted(() => {
+ redirectAfterLogin();
+ }),
+ [hanko, redirectAfterLogin]
+ );
+
+ useEffect(() => {
+ //
+ register(hankoApi ?? "").catch((error) => {
+ console.log(error);
+ });
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+}
+
+```
+
+
+## Time to display the right Todos 🪄✨
+Lastly, we should only display the todos for the user that is logged in. To do so, we need to link the todos to the correct "user ID". The first step is to update the Todo model in the `prisma schema`:
+
+```javascript
+model Todo {
+ userId String
+ id String @id @default(uuid())
+ title String
+ complete Boolean
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+```
+Then run the following command to create a migration:
+
+```
+npx prisma migrate
+```
+Or the following to push the schema changes directly to the database:
+
+```
+npx prisma db push
+```
+Next step is to update the `api/todo/route.tsx` file to get the user ID from the token, then create a new todo if there is a user ID:
+
+```javascript
+import { NextResponse } from "next/server";
+import { cookies } from "next/headers";
+import * as jose from "jose";
+import { prisma } from "@/db";
+
+export async function userId() {
+ const token = cookies().get("hanko")?.value;
+ const payload = jose.decodeJwt(token ?? "");
+
+ return payload.sub;
+}
+
+export async function POST(req: Request) {
+ const userID = await userId();
+ const { title } = await req.json();
+
+ if (userID) {
+ if (typeof title !== "string" || title.length === 0) {
+ throw new Error("That can't be a title");
+ }
+ await prisma.todo.create({
+ data: { title, complete: false, userId: userID ?? "" },
+ });
+
+ return NextResponse.json({ message: "Created Todo" }, { status: 200 });
+ } else {
+ return NextResponse.json({ error: "Not Found" }, { status: 404 });
+ }
+}
+
+```
+
+The final step is to update the Prisma call to fetch all the todos from the `todo/page.tsx`:
+
+```javascript
+import { Logout } from "@/components/Logout";
+import { Profile } from "@/components/Profile";
+import { NewTodo } from "@/components/todos/NewTodo";
+import { TodoItem } from "@/components/todos/TodoItem";
+import { prisma } from "@/db";
+import { userId } from "../api/todo/route";
+
+export default async function Todo() {
+ const userID = await userId();
+
+ const todos = await prisma.todo.findMany({
+ where: {
+ userId: { equals: userID },
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
My to dos
+
+
+
+
+
+
+ );
+}
+
+```
+
+That’s all, you’ve successfully created a Todo app with Next.js, Hanko, Prisma and Tailwind CSS!
From a23d03828733cfb770572a396fd05f5b0f65ac02 Mon Sep 17 00:00:00 2001
From: Esther Lizardo <100808713+Esther-Lita@users.noreply.github.com>
Date: Wed, 13 Sep 2023 10:17:01 +0200
Subject: [PATCH 02/23] Updated text on Next.js
---
docs/docs/guides/tutorials/Nextjs.mdx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/docs/docs/guides/tutorials/Nextjs.mdx b/docs/docs/guides/tutorials/Nextjs.mdx
index eed699d40..b66602b82 100644
--- a/docs/docs/guides/tutorials/Nextjs.mdx
+++ b/docs/docs/guides/tutorials/Nextjs.mdx
@@ -97,7 +97,6 @@ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
ℹ️ For more information about Prisma integration click [here](https://www.prisma.io/docs/getting-started).
-
## Building the user interface
The goal is to build a simple "todo app" with a nice login to protect the todos, for this we will only need two pages:
* The login page where the Hanko-auth component will play its part in handling authentication.
@@ -263,7 +262,7 @@ export async function POST(req: Request) {
}
```
-Since everything on Next.js 13 runs in the server, we can now call Prisma from the `todo/page.tsx` file to get all our todos, then we pass them to our `components/todos/TodoItem.tsx` file to be displayed. This is how the `todo/page.tsx` should look after our changes:
+By default, Next.js uses Server Components, thanks to that we can now call Prisma from the `todo/page.tsx` file to get all our todos, then we pass them to our `components/todos/TodoItem.tsx` file to be displayed. This is how the `todo/page.tsx` should look after our changes:
```javascript
import { NewTodo } from "@/components/todos/NewTodo";
@@ -650,7 +649,6 @@ export default function Login() {
```
-
## Time to display the right Todos 🪄✨
Lastly, we should only display the todos for the user that is logged in. To do so, we need to link the todos to the correct "user ID". The first step is to update the Todo model in the `prisma schema`:
From f8f2fb1bca8c39728d101b1fd675af42187c974d Mon Sep 17 00:00:00 2001
From: litaesther10
Date: Thu, 14 Sep 2023 14:35:19 +0200
Subject: [PATCH 03/23] Added local images
---
docs/docs/guides/tutorials/Nextjs.mdx | 223 ++++++++++--------
docs/sidebars.js | 4 +-
.../img/next-tutorial/cloud-organization.png | Bin 0 -> 43867 bytes
docs/static/img/next-tutorial/dashboard.png | Bin 0 -> 75373 bytes
docs/static/img/next-tutorial/hanko-auth.png | Bin 0 -> 15993 bytes
.../img/next-tutorial/hanko-profile.png | Bin 0 -> 30932 bytes
docs/static/img/next-tutorial/new-project.png | Bin 0 -> 67489 bytes
docs/static/img/next-tutorial/next-config.png | Bin 0 -> 130664 bytes
8 files changed, 128 insertions(+), 99 deletions(-)
create mode 100644 docs/static/img/next-tutorial/cloud-organization.png
create mode 100644 docs/static/img/next-tutorial/dashboard.png
create mode 100644 docs/static/img/next-tutorial/hanko-auth.png
create mode 100644 docs/static/img/next-tutorial/hanko-profile.png
create mode 100644 docs/static/img/next-tutorial/new-project.png
create mode 100644 docs/static/img/next-tutorial/next-config.png
diff --git a/docs/docs/guides/tutorials/Nextjs.mdx b/docs/docs/guides/tutorials/Nextjs.mdx
index b66602b82..914af74b1 100644
--- a/docs/docs/guides/tutorials/Nextjs.mdx
+++ b/docs/docs/guides/tutorials/Nextjs.mdx
@@ -1,37 +1,49 @@
+---
+title: Next.js and Hanko Tutorial
+sidebar_label: Next.js
+keywords: [next, next.js]
+sidebar_custom_props:
+ docCardIconName: nextjs-dark
+---
+
# Using Hanko for a Todo app with Next.js 13 and Prisma
In this tutorial, you’ll learn how to build a Todo app with the Next.js 13 popular “App Router” structure and understand some of the most important changes that come with it. We will build a fully functional todo app, handling the creation, updating when completed and the option to delete the todos.
-## Let’s set it up
-At Hanko, we understand that finding the right stack for the project is a big step. In this guide, we will bring Next.js as the main character of the project, we will test the Client vs Server Components. For the style, we will use Tailwind CSS. We will use Hanko for the login and registration, user management and logout. Prisma will handle the storage.
+## Let’s set it up
+
+At Hanko, we understand that finding the right stack for the project is a big step. In this guide, we will bring Next.js as the main character of the project, we will test the Client vs Server Components. For the style, we will use Tailwind CSS. We will use Hanko for the login and registration, user management and logout. Prisma will handle the storage.
### Initiliaze the next app
-To create a new Next.js app, we can use the `create-next-app` or `create-next-app@latest` command-line tool followed by the name of your choice for the app. Open your terminal in Visual Studio Code and run the following command:
+
+To create a new Next.js app, we can use the `create-next-app` or `create-next-app@latest` command-line tool followed by the name of your choice for the app. Open your terminal in Visual Studio Code and run the following command:
```
- npx create-next-app@latest todo-nextjs-hanko
+ npx create-next-app@latest todo-nextjs-hanko
```
-Then you’ll be asked some prompts on what you will use for the app. The project configuration options should look something like this:
+Then you’ll be asked some prompts on what you will use for the app. The project configuration options should look something like this:
-![Pre-config Next.js prompts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8djxl7jl42xl90wpps1b.png)
+![Pre-config Next.js prompts](/img/next-tutorial/next-config.png)
The above choices will create a new Next.js app with the chosen name, all the required dependencies for this project will also be installed.
#### Understanding the project structure
-When using the version 13 of Next.js, we have the option to work with the App Router directory instead of the Pages Router, for a quick summary we could say that:
-* The new directory named “app” is replacing “pages”
-* “page.tsx|page.jsx” is the new “index.tsx|index.jsx”
-* “layout.tsx” is the new “_app.tsx”
-* Everything is a Server Component unless you make it a Client Component using the “use client” directive at the top of the file.
-* API Routes are now Server Components or Route Handlers (... More info on how this is very important)
+When using the version 13 of Next.js, we have the option to work with the App Router directory instead of the Pages Router, for a quick summary we could say that:
+
+- The new directory named “app” is replacing “pages”
+- “page.tsx|page.jsx” is the new “index.tsx|index.jsx”
+- “layout.tsx” is the new “\_app.tsx”
+- Everything is a Server Component unless you make it a Client Component using the “use client” directive at the top of the file.
+- API Routes are now Server Components or Route Handlers (... More info on how this is very important)
Remove unnecessary files, such as logos, icons, etc. If you are going to use Tailwind CSS make sure to bring your desired configuration to the `tailwind.config.ts` file, defining your color palette, fonts, breakpoints, ect.
ℹ️ For more information about the App Router of Next.js click [here](https://nextjs.org/docs).
-### Get Prisma started
+### Setting up Prisma
+
Install the Prisma CLI as a development dependency in the project:
```
@@ -44,6 +56,7 @@ Set up Prisma with the init command of the Prisma CLI:
$ npx prisma init --datasource-provider sqlite
```
+
This creates a new `prisma` directory with your Prisma schema file and configures SQLite as your database. Once we also create the "Todo" model, the Prisma schema file should look like this:
```
@@ -60,7 +73,7 @@ datasource db {
}
model Todo {
-
+
id String @id @default(uuid())
title String
complete Boolean
@@ -68,11 +81,13 @@ model Todo {
updatedAt DateTime @updatedAt
}
```
+
At this point, you have a Prisma schema but no database yet. Run the following command in your terminal to create the SQLite database and the Todo table:
```
$ npx prisma migrate dev --name init
```
+
This command did two things:
1. It creates a new SQL migration file for this migration in the `prisma/migrations` directory.
@@ -80,9 +95,9 @@ This command did two things:
Because the SQLite database file didn't exist before, the command also created it inside the `prisma` directory with the name `dev.db` as defined via the environment variable in the `.env` file.
-To prevent problems when instantiating PrismaClient, on the Prisma Docs there’s a [section](https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices) dedicated to the best practice to do it. Let’s try it by creating a `db.ts` file in the root of the app and add the following code inside:
+To prevent problems when instantiating PrismaClient, on the Prisma Docs there’s a [section](https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices) dedicated to the best practice to do it. Let’s try it by creating a `db.ts` file in the root of the app and add the following code inside:
-```javascript
+```js
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
@@ -94,35 +109,39 @@ export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
```
+
ℹ️ For more information about Prisma integration click [here](https://www.prisma.io/docs/getting-started).
+## Building the user interface
-## Building the user interface
The goal is to build a simple "todo app" with a nice login to protect the todos, for this we will only need two pages:
-* The login page where the Hanko-auth component will play its part in handling authentication.
-* The todo page where all the todos will be displayed.
-### App structure
+- The login page where the Hanko-auth component will play its part in handling authentication.
+- The todo page where all the todos will be displayed.
+
+### App structure
+
In the App Router directory, the `page.tsx` is like the new `index.tsx`, which means that this name will play an important role when creating a new route. You can define a page by exporting a component from a `page.tsx` file.
Now you can update the `page.tsx` file to display "Hello World" as done below.
-```javascript
- export default function Home() {
-return (
-
+ );
}
```
- We will get back to it later to add a nice login with Hanko.
+We will get back to it later to add a nice login with Hanko.
+
+### The Todo page
-### The Todo page
We will style this page using Tailwind CSS classes to create a centered container to display the todos. we need a form with an input to create the new todos, and every todo element will have a checkbox and a delete button. Inside the `app` directory, create a new `todo` folder with a `page.tsx` file inside of it. Use the code below as the `todo/page.tsx` contents:
-```javascript
+```js
export default function Todo() {
return (
@@ -138,9 +157,7 @@ export default function Todo() {
required
/>
@@ -168,16 +185,16 @@ export default function Todo() {
);
}
-
```
ℹ️ For a better understanding of the Tailwind CSS classes click [here](https://tailwindcomponents.com/cheatsheet/).
+## Todos in the making
-## Todos in the making
-To make our app functional, we need to be able to create a new todo, then check the todo once it’s completed and finally be able to remove a single todo from the list.
+To make our app functional, we need to be able to create a new todo, then check the todo once it’s completed and finally be able to remove a single todo from the list.
### API Routes in Next.js 13
+
What we know as API Routes are replaced by Route Handlers and they are defined in a `route.ts|js` file inside the `app` directory. Read more about the Route Handlers in the [Next.js Docs](https://nextjs.org/docs/app/building-your-application/routing/route-handlers).
Inside the `app` directory create an `api` folder. We will group our Route Handlers as follows: one directory `todo` with a `route.tsx` which will contain the `POST` HTTP method handler for creating a new todo, and in that same directory we will use a dynamic route to `GET` and `DELETE` todos. Should look like the following example:
@@ -191,9 +208,10 @@ api
```
### New Todo
+
This is a good moment to start breaking it down into components, let’s first create a `components` folder at the root directory, then create a `components/todos/NewTodo.tsx` file and use the following as its contents:
-
-```javascript
+
+```js
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
@@ -239,15 +257,14 @@ export const NewTodo = () => {
);
-}
-
+};
```
-This is a good example of where to use the "use client" directive since we are using `useState()` and subscribing to interactive events.
+This is a good example of where to use the "use client" directive since we are using `useState()` and subscribing to interactive events.
This is how we call Prisma to create the todo inside the `api/todo/route.ts` Route Handler:
-```javascript
+```js
import { NextResponse } from "next/server";
import { prisma } from "@/db";
@@ -262,9 +279,9 @@ export async function POST(req: Request) {
}
```
-By default, Next.js uses Server Components, thanks to that we can now call Prisma from the `todo/page.tsx` file to get all our todos, then we pass them to our `components/todos/TodoItem.tsx` file to be displayed. This is how the `todo/page.tsx` should look after our changes:
+By default, Next.js uses Server Components, thanks to that we can now call Prisma from the `todo/page.tsx` file to get all our todos, then we pass them to our `components/todos/TodoItem.tsx` file to be displayed. This is how the `todo/page.tsx` should look after our changes:
-```javascript
+```js
import { NewTodo } from "@/components/todos/NewTodo";
import { TodoItem } from "@/components/todos/TodoItem";
import { prisma } from "@/db";
@@ -278,19 +295,21 @@ export default async function Todo() {
My to dos
-
+
);
}
```
+
🚨 Client Components themselves cannot be async functions ([official FAQ](https://nextjs.org/docs/messages/no-async-client-component)). Prisma will break the app if you try to call it inside a Client Component.
-### Update and Delete todo by ID
+### Update and Delete todo by ID
+
In the next step, we need a way to handle marking a todo as completed and to handle the deletion of a todo. Accordingly, we create `update` and `delete` functions that fetch our dynamic route. This would be the `components/todos/TodoItem.tsx` file:
-```javascript
+```js
"use client";
import { useRouter } from "next/navigation";
import { Todo } from "@prisma/client";
@@ -359,7 +378,7 @@ export const TodoItem = ({ todos }: { todos: Todo[] }) => {
Add the following code inside the `api/todo/[id]/route.tsx` Route Handler:
-```javascript
+```js
import { NextResponse } from "next/server";
import { prisma } from "@/db";
@@ -394,34 +413,37 @@ export async function DELETE(req: Request) {
ℹ️ For more information on the Prisma Client Api click [here](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference).
-
## Authentication by Hanko
+
Now is time to start working on the security.
-
-### Hanko Cloud setup
-Visit [Hanko Cloud](https://cloud.hanko.io/login) and create an account. Then create an organization to manage your Hanko project.
-![Create a Hanko Organization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8vho8qsrbkkthn6602t.png)
+### Hanko Cloud setup
+
+Visit [Hanko Cloud](https://cloud.hanko.io/login) and create an account. Then create an organization to manage your Hanko project.
+
+![Hanko Cloud Organization](/img/next-tutorial/cloud-organization.png)
Then create a new project and set the App URL to your development URL (example: http://localhost:3000):
-![Hanko Cloud Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hgfioy9p3wet6t5fe7sn.png)
+![Hanko Cloud Project](/img/next-tutorial/new-project.png)
And that’s all! Now you can always return to your Hanko Cloud dashboard to see your API URL and other insights about your project, you can also change the app URL in the settings, so that once you want to move from "development" to "production", you can change it to a proper domain/URL. Take the time to discover all the features.
-![Hanko Cloud dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvan097duorvng3c1my0.png)
+![Hanko Cloud dashboard](/img/next-tutorial/dashboard.png)
### Adding Hanko to the Next.js app
-Let’s bring Hanko to the game by installing the package running the code below:
+
+Let’s bring Hanko to the game by installing the package running the code below:
```
npm install @teamhanko/hanko-elements
```
+
First, let’s update our "Home" page and rename the function to "Login". Import the register function from `@teamhanko/hanko-elements`, and call the function with the Hanko API URL as an argument to register the ``. Now include it in your JSX:
-```javascript
+```js
"use client";
-import { useEffect} from "react";
+import { useEffect } from "react";
import { register } from "@teamhanko/hanko-elements";
const hankoApi = "YOUR_HANKO_API_URL";
@@ -440,13 +462,14 @@ export default function Login() {
);
}
```
+
The code snippet above should display the Hanko authentication component:
-![Hanko Authentication component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ma10mk3z4sk7h925v03t.png)
+![Hanko Authentication component](/img/next-tutorial/hanko-auth.png)
- The `` component offers a page for managing email addresses and passkeys. Let's create a profile button component by creating a file `components/Profile.tsx` and use the following code as its content:
+The `` component offers a page for managing email addresses and passkeys. Let's create a profile button component by creating a file `components/Profile.tsx` and use the following code as its content:
-```javascript
+```js
"use client";
import { useEffect, useState } from "react";
import { register } from "@teamhanko/hanko-elements";
@@ -482,13 +505,14 @@ export const Profile = () => {
);
};
```
+
It should look like this:
-![Hanko Profile Component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmp0lj2lfwdg5kdzlp5c.png)
+![Hanko Profile Component](/img/next-tutorial/hanko-profile.png)
Now let’s use `@teamhanko/hanko-elements` to manage user logouts by creating a logout button component. Create a file `components/Logout.tsx` and use the following as its content:
-```javascript
+```js
"use client";
import { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/navigation";
@@ -525,32 +549,33 @@ export const Logout = () => {
);
};
```
- When a user logs out, a specific event is triggered that you can subscribe to, like redirecting to a specific page after logout:
-```javascript
- const renewSession = useCallback(() => {
- router.replace("/");
- }, [router]);
+When a user logs out, a specific event is triggered that you can subscribe to, like redirecting to a specific page after logout:
- useEffect(
- () =>
- hanko?.onSessionExpired(() => {
- renewSession();
- }),
+```js
+const renewSession = useCallback(() => {
+ router.replace("/");
+}, [router]);
- [hanko, renewSession]
- );
+useEffect(
+ () =>
+ hanko?.onSessionExpired(() => {
+ renewSession();
+ }),
+
+ [hanko, renewSession]
+);
```
+
ℹ️ For more information about all the events that you can "listen" from the Hanko client click [here](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#events).
ℹ️ You can also find information on how to customize Hanko Components by clicking [here](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#ui-customization).
-
## Verifying JWT with jose library
-The JWT is signed by Hanko and to secure our app we still need to verify the JWT.
-> **What are JWTs?**
-> _A JSON Web Token (JWT) is a compact and self-contained way for transmitting information between parties as a JSON object in a secure way. The purpose of a JWT is to ensure the authenticity of the data._
+The JWT is signed by Hanko and to secure our app we still need to verify the JWT.
+
+> **What are JWTs?** > _A JSON Web Token (JWT) is a compact and self-contained way for transmitting information between parties as a JSON object in a secure way. The purpose of a JWT is to ensure the authenticity of the data._
Hanko handles the authentication and signing of the JWT, on successful authentication with Hanko a cookie, which contains said JWT as its value, is set. we don’t really need to know a lot about them but it’s worth getting familiar with the parts of a JWT (header, payload and signature), and what is a [JWKS](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets), for more information you can visit [JWT.io](https://jwt.io/).
@@ -559,14 +584,16 @@ To verify the JWT we need to install the `jose-jwt` package:
```
npm i jose
```
-Jose is a JavaScript module that supports JWT and provides functionality for signing and verifying tokens.
-ℹ️ For more information about Jose click [here](https://www.npmjs.com/package/jose).
+Jose is a JavaScript module that supports JWT and provides functionality for signing and verifying tokens.
+
+ℹ️ For more information about Jose click [here](https://www.npmjs.com/package/jose).
-### Middleware
-Create a new file `middleware.tsx` in the root of your project and use the following code:
+### Middleware
-```javascript
+Create a new file `middleware.tsx` in the root of your project and use the following code:
+
+```js
import * as jose from "jose";
import { NextRequest } from "next/server";
@@ -583,18 +610,20 @@ export default async function middleware(req: NextRequest) {
const verifiedJWT = await jose.jwtVerify(token, JWKS);
console.log(verifiedJWT);
} catch {
-return NextResponse.redirect(new URL("/", req.url));
-}
+ return NextResponse.redirect(new URL("/", req.url));
+ }
}
```
+
To verify the JWT we need the token and the JWKS. We get the token from the "hanko" cookie, and then we obtain the JSON Web Key Set (JWKS) calling the `createRemoteJWKSet` function from jose. Then we call `await jose.jwtVerify(token, JWKS)`. If the token can be verified, then the promise returned from the function resolves to a decoded token. If it cannot be verified, then the promise rejects and we can catch the error and handle it appropriately, e.g. by redirecting the user to the login/home page. If you console.log the const `verifiedJWT` you should see the decoded token showing the payload, the protectedHeader and the key. Inside the key, you should be able to see a "true" if it’s verified.
ℹ️ For more information about Next.js Middleware click [here](https://nextjs.org/docs/app/building-your-application/routing/middleware#conditional-statements).
-### Securing the application and redirecting
+### Securing the application and redirecting
+
To prevent unauthorized users from getting access to private user data, we can add the paths to be protected in the Middleware configuration. Copy the following code at the bottom of your `middleware.tsx` file:
-```javascript
+```js
export const config = {
matcher: ["/todo"],
};
@@ -602,7 +631,7 @@ export const config = {
Update the `Login` page to subscribe to the events of the Hanko client and redirect to the `Todo` page after a successful login:
-```javascript
+```js
"use client";
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
@@ -649,10 +678,11 @@ export default function Login() {
```
-## Time to display the right Todos 🪄✨
+## Time to display the right Todos
+
Lastly, we should only display the todos for the user that is logged in. To do so, we need to link the todos to the correct "user ID". The first step is to update the Todo model in the `prisma schema`:
-```javascript
+```
model Todo {
userId String
id String @id @default(uuid())
@@ -662,19 +692,22 @@ model Todo {
updatedAt DateTime @updatedAt
}
```
+
Then run the following command to create a migration:
```
npx prisma migrate
```
+
Or the following to push the schema changes directly to the database:
```
npx prisma db push
```
+
Next step is to update the `api/todo/route.tsx` file to get the user ID from the token, then create a new todo if there is a user ID:
-```javascript
+```js
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import * as jose from "jose";
@@ -704,12 +737,11 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Not Found" }, { status: 404 });
}
}
-
```
The final step is to update the Prisma call to fetch all the todos from the `todo/page.tsx`:
-```javascript
+```js
import { Logout } from "@/components/Logout";
import { Profile } from "@/components/Profile";
import { NewTodo } from "@/components/todos/NewTodo";
@@ -744,7 +776,6 @@ export default async function Todo() {
);
}
-
```
-That’s all, you’ve successfully created a Todo app with Next.js, Hanko, Prisma and Tailwind CSS!
+That’s all, you’ve successfully created a Todo app with Next.js, Hanko, Prisma and Tailwind CSS!
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 4dd6aa649..1abaabf3f 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -69,9 +69,7 @@ const sidebars = {
{
type: "category",
label: "Tutorials",
- items: [
- "guides/tutorials/php",
- ],
+ items: ["guides/tutorials/php", "guides/tutorials/nextjs"],
},
],
};
diff --git a/docs/static/img/next-tutorial/cloud-organization.png b/docs/static/img/next-tutorial/cloud-organization.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c6386099715dba5ff5ece86e4bd49f08fe68766
GIT binary patch
literal 43867
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