DevGang
Авторизоваться

💥 Создание приложения Todo Next.js 13 с использованием Prisma и аутентификации по паролю от Hanko 🤯

Привет, читатели блога dev-gang!

В этом руководстве вы узнаете, как создать приложение Todo с помощью популярной структуры «App Router» Next.js 13, а также поймете некоторые из наиболее важных изменений, которые с ней связаны.

Мы создадим полнофункциональное приложение Todo.

  • Создать Todo
  • Проверить и удалить один элемент
  • Удалить все задачи

Мы будем использовать Hanko для:

  • Входа в систему и регистрации
  • Управления пользователями
  • Выхода из системы

Prisma позаботится о хранении.

С чего нам начать? 👷🏻‍♂️

Работа со сложными фреймворками может лишить удовольствия от прекрасного процесса создания чего-то с нуля, поэтому найти правильный стек для нашего проекта — большой шаг. В этом руководстве я решил сделать Next.js главным героем этого проекта из-за прекрасной возможности протестировать сумасшедший поворот, который они привнесли со всеми этими реализациями «использовать сервер» и «использовать клиент». Кроме того, когда вы создаете новое приложение, Next.js упрощает задачу, предоставляя вам возможность интегрировать все, что вам, вероятно, понадобится, например Typescript, ESLint и Tailwind CSS, и да, мы будем использовать их все!

Tailwind CSS

Для большей части проекта я буду использовать Tailwind CSS для стилизации, потому что я считаю, что его очень легко реализовать и вдохнуть жизнь в приложение, а когда дело доходит до обслуживания кода, его легко читать и сразу понять, что происходит в приложении. точный элемент, а Tailwind просто делает работу с CSS приятной.

Prisma

В этом приложении нам не нужно многого, когда речь идет о том, где мы будем хранить данные, и Prisma просто идеально подходит для того, что нам нужно для правильного создания, обновления и/или удаления «задач».

Hanko

Когда дело доходит до защиты приложения, первое, что вступает в игру, — это сочетание создания, настройки и оформления входа в систему и регистрации на внешнем интерфейсе, а затем сложность серверной части с функциональностью, аутентификацией, обработкой ошибок и все детали, необходимые для успешного и безопасного «сеанса пользователя». Именно здесь Ханко входит в игру, беря на себя ответственность за вход в систему, регистрацию, управление пользователями и выход из системы, давая нам возможность уделять больше времени и сосредоточиться на других частях проекта.

«Создайте лучший логин, который вы когда-либо видели»

Hanko — это решение для аутентификации с открытым исходным кодом, призванное вывести процесс разработки на новый уровень. Вы можете решить, какой тип информации потребуется вашему приложению для входа в систему, например адрес электронной почты и пароль, ключи доступа, пароли электронной почты и сторонние поставщики удостоверений. Затем вы можете проявлять сколь угодно творческий подход к стилизации веб-компонентов Hanko.

Мы готовы его установить 🆙

Здесь я расскажу вам, как создать настройку проекта для приложения. Убедитесь, что у вас установлена ​​последняя версия Node.js.

Создайте новое приложение Next.js.

Чтобы создать новое приложение Next.js, мы можем использовать инструмент командной строки create-next-app или create-next-app@latest, за которым следует имя приложения по вашему выбору. Откройте терминал в Visual Studio Code и выполните следующую команду:

 npx  create-next-app@latest todo-nextjs-hanko 

Это запустит процесс создания проекта, после чего вам будет предложено несколько подсказок о том, что вы будете использовать для приложения. Параметры конфигурации проекта должны выглядеть примерно так:

Вышеуказанные варианты создадут новое приложение Next.js с выбранным именем, также будут установлены все необходимые зависимости для этого проекта.

Понимание структуры проекта

При использовании версии 13 Next.js у нас есть возможность работать с каталогом App Router вместо Pages Router. Для краткого обзора мы могли бы сказать, что:

  • Новый каталог с именем app заменяет pages.
  • page.tsx|page.jsx — это новый index.tsx|index.jsx.
  • layout.tsx — это новый _app.tsx.
  • Все является серверным компонентом, если вы не сделаете его клиентским компонентом с помощью директивы use client в верхней части файла.
  • Маршруты API теперь являются компонентами сервера или обработчиками маршрутов.

Удалите ненужные файлы, такие как логотипы, значки и т. д. Если вы собираетесь использовать Tailwind CSS, обязательно добавьте желаемую конфигурацию в файл Tailwind.config.ts, определив цветовую палитру, шрифты, точки останова и т. д.

Для получения дополнительной информации о App Router Next.js нажмите здесь.

Начать работу с Prisma

Установите Prisma CLI в качестве зависимости разработки в проекте:

 npm install prisma --save-dev

Настройте Prisma с помощью команды init в интерфейсе командной строки Prisma:

 npx prisma init --datasource-provider sqlite

Это создаст новый каталог prisma с вашим файлом схемы Prisma и настроит SQLite в качестве вашей базы данных. Как только мы создадим модель Todo, файл схемы Prisma должен выглядеть следующим образом:

// 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
}

На данный момент у вас есть схема Prisma, но еще нет базы данных. Запустите следующую команду в своем терминале, чтобы создать базу данных SQLite и таблицу Todo:

 $ npx prisma migrate dev --name init

Эта команда сделала две вещи:

  1. Для этой миграции создается новый файл миграции SQL в каталоге prisma/migrations.
  2. Он запускает файл миграции SQL для базы данных.

Поскольку файла базы данных SQLite раньше не существовало, команда также создала его внутри каталога prisma с именем dev.db, определенным через переменную среды в файле .env.

Чтобы предотвратить проблемы при создании экземпляра PrismaClient, в документации Prisma есть раздел, посвященный лучшим практикам этого. Давайте попробуем, создав файл db.ts в корне приложения и добавив внутрь следующий код:

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;

Для получения дополнительной информации об интеграции Prisma нажмите здесь.

Создание пользовательского интерфейса 🛠️

Учитывая, что мы хотим создать простое todo app с удобным логином для защиты задач, нам понадобятся всего две страницы:

  • Страница входа, на которой компонент Hanko-auth будет играть свою роль в обработке аутентификации.
  • Страница задач, на которой будут отображаться все задачи.

Структура приложения

В каталоге App Router файл page.tsx аналогичен новому index.tsx, а это означает, что это имя будет играть важную роль при создании нового маршрута. Вы можете определить страницу, экспортировав компонент из файла page.tsx.

Теперь вы можете обновить файл page.tsx, чтобы в нем отображалось «Hello World», как показано ниже.

 export default function Home() {
return (
<div> 
   <p>Hello World</p>
</div>
);
}

Мы вернемся к этому позже, чтобы добавить удобный вход в систему с помощью Ханко.

Страница задач

Мы оформим эту страницу с помощью CSS-классов Tailwind, чтобы просто создать центрированный контейнер для отображения задач. Нам нужна форма с полем ввода для создания новых задач, и каждый элемент задачи будет иметь флажок и кнопку удаления. Внутри каталога приложения создайте новую папку задач с файлом page.tsx внутри нее. Используйте приведенный ниже код в качестве содержимого todo/page.tsx:

export default function Todo() {
  return (
    <main className=" flex min-h-screen justify-center items-center bg-slate-50 ">
      <div className="bg-slate-300 rounded-3xl py-6  h-[400px] w-[450px] flex flex-col text-slate-800">
        <h1 className="text-3xl text-center">My to dos</h1>
        <div className="mx-8 mt-4 mb-6">
          <form className="flex gap-3 items-center">
            <input
              type="text"
              name="title"
              placeholder="New todo"
              className=" border border-slate-400 rounded-full flex-1  py-1 px-2 outline-none focus-within:border-slate-100 bg-slate-50 focus-within:bg-slate-100 placeholder:text-slate-300"
              required
            />
            <button className="  bg-slate-50 rounded-full p-1 border border-slate-400 text-slate-400 hover:text-slate-500 text-base hover:ring-0 hover:ring-slate-100 hover:border-slate-500">
              <p className=" text-center">
             +
              </p>
            </button>
          </form>
        </div>
        <ul className="px-6">
          <li className="flex px-4">
            <span className="flex gap-2 flex-1">
              <input
                type="checkbox"
                name="check"
                className="peer cursor-pointer accent-slate-300 "
              />
              <label
                htmlFor=""
                className="peer-checked:line-through peer-checked:text-slate-500 cursor-pointer"
              >
                Todo 1
              </label>
            </span>
            <button className="text-slate-500  hover:text-slate-800 mr-3">
              X
            </button>
          </li>
        </ul>
      </div>
    </main>
  );
}

🚨 Для улучшения пользовательского интерфейса используйте SVG или значки внутри кнопок.

Чтобы лучше понять CSS-классы Tailwind, нажмите здесь.

Todos в разработке 🚧

Чтобы наше приложение работало, нам нужно иметь возможность создавать новую задачу, затем проверять задачу после ее завершения и, наконец, иметь возможность удалять одну задачу из списка.

Маршруты API в Next.js 13

При использовании App Router из Next.js 13 маршруты API заменяются обработчиками маршрутов, и они определяются в файле Route.ts|js внутри каталога приложения. На том же уровне сегмента маршрута, что и page.tsx, не может быть файла route. Подробнее об обработчиках маршрутов читайте в документации Next.js.

Внутри каталога приложения создайте папку API. Мы сгруппируем наши обработчики маршрутов следующим образом: один каталог todo с файлом Route.ts, который будет содержать обработчик метода POST HTTP для создания нового todo, и в этом же каталоге мы будем использовать динамический маршрут для задач GET и DELETE. Должно выглядеть как следующий пример:

api
└── todo
    ├── [id]
    │   └── route.ts
    └── route.ts

Новое Todo

Начнем с создания задачи. Это хороший момент, чтобы начать разбивать его на компоненты. Давайте сначала создадим папку components в корневом каталоге, затем создадим файл Components/todos/NewTodo.tsx и будем использовать в качестве его содержимого следующее:

"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 (
    <div className="mx-8 mt-4 mb-6">
      <form onSubmit={create} className="flex gap-3 items-center">
        <input
          type="text"
          name="title"
          value={newItem}
          onChange={(e) => setNewItem(e.target.value)}
          placeholder="New todo"
          className=" border border-slate-400 rounded-full flex-1  py-1 px-2 outline-none focus-within:border-slate-100 bg-slate-50 focus-within:bg-slate-100 placeholder:text-slate-300"
          required
        />
        <button
          type="submit"
          className="  bg-slate-50 rounded-full p-1 border border-slate-400 text-slate-400 hover:text-slate-500 text-base hover:ring-0 hover:ring-slate-100 hover:border-slate-500"
        >
          <p className=" text-center">+</p>
        </button>
      </form>
    </div>
  );
}

Это хороший пример того, как добавить в игру директиву use client, поскольку мы используем useState() и подписываемся на интерактивные события.

Вот как мы вызываем Prisma для создания задачи внутри обработчика маршрутов api/todo/route.ts:

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 });
}

Вы можете решить, как проверять правильность передаваемых значений и как обрабатывать ошибки в обработчиках маршрутов.

Давайте проверим «все серверные компоненты», пока не скажем обратное из Next.js 13, вызвав Prisma из файла todo/page.tsx, чтобы получить все наши задачи, затем передадим их в наш файл Components/todos/TodoItem.tsx для быть отображены. Вот как должен выглядеть файл todo/page.tsx после наших изменений:

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 (
    <main className=" flex min-h-screen justify-center items-center bg-slate-50 ">
      <div className="bg-slate-300 rounded-3xl py-6  h-[400px] w-[450px] flex flex-col text-slate-800">
        <h1 className="text-3xl text-center">My to dos</h1>
        <NewTodo />
        <ul className="px-6">
        <TodoItem  todos={todos} />
        </ul>
      </div>
    </main>
  );
}

🚨 Клиентские компоненты сами по себе не могут быть асинхронными функциями (официальный FAQ). А Prisma сломает приложение, если вы попытаетесь вызвать его внутри клиентского компонента.

Обновить и удалить задачу по идентификатору ✅

Теперь у нас уже есть способ создать новую задачу и отобразить список всех задач. На следующем шаге нам нужен способ обработки маркировки задачи как выполненной и обработки удаления задачи. Соответственно, мы создаем функции обновления и удаления, которые извлекают наш динамический маршрут. Это будет файл Components/todos/TodoItem.tsx:

"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 (
          <li key={todo.id} className="flex px-4">
            <span className="flex gap-2 flex-1">
              <input
                type="checkbox"
                name="check"
                checked={todo.complete}
                onChange={() => update(todo)}
                className="peer cursor-pointer accent-slate-300 "
              />
              <label
                htmlFor={todo.id}
                className="peer-checked:line-through peer-checked:text-slate-500 cursor-pointer"
              >
                {todo.title}
              </label>
            </span>
            <button
              onClick={() => deleteTodo(todo)}
              className="text-slate-500  hover:text-slate-800 mr-3"
            >
              X
            </button>
          </li>
        );
      })}
    </>
  );
};

Добавьте следующий код в обработчик маршрутов api/todo/[id]/route.ts:

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 });
}

Для получения дополнительной информации об API Prisma Client нажмите здесь.

Аутентификация Hanko

К этому моменту у вас должно быть запущено полнофункциональное приложение todo. Нам следует начать работать над безопасностью.

Если вы знакомы с Hanko, вы можете пропустить следующий шаг или продолжайте читать, чтобы начать работу с Hanko Cloud и запустить свой Hanko API.

Настройка облака Hanko ☁️

Посетите Hanko Cloud и создайте учетную запись. Затем создайте организацию для своего проекта Hanko.

Затем создайте новый проект и установите в качестве URL-адреса приложения URL-адрес разработки.

И это все! Теперь вы всегда можете вернуться на панель управления Hanko Cloud, чтобы увидеть URL-адрес вашего API и другую информацию о вашем проекте. Вы также можете изменить URL-адрес приложения в настройках, чтобы, как только вы захотите перейти от «development» к «production», вы можно изменить его на правильный domain/URL. Потратьте время, чтобы открыть для себя все возможности.

Добавление Hanko в приложение Next.js

Hanko — это легкое и простое в реализации решение для аутентификации пользователей, которое значительно упрощает переход к паролям. Давайте добавим Hanko в игру, установив пакет, выполняющий приведенный ниже код:

npm install @teamhanko/hanko-elements

Сначала давайте обновим нашу Home страницу и переименуем функцию в Login. Импортируйте функцию регистрации из @teamhanko/hanko-elements и вызовите функцию с URL-адресом Hanko API в качестве аргумента для регистрации <hanko-auth>. Теперь включите его в свой JSX:

"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 (
    <div className="flex min-h-screen justify-center items-center ">
      <hanko-auth />
    </div>
  );
}

В приведенном выше фрагменте кода должен отображаться компонент аутентификации Hanko:

Компонент <hanko-profile> предлагает страницу для управления адресами электронной почты и ключами доступа, и мы хотим получить к ней доступ. Давайте создадим компонент кнопки профиля, создав файл Components/Profile.tsx и используя следующий код в качестве его содержимого:

"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 (
    <>
      <button type="button" onClick={openProfile}>
        Profile
      </button>
      {openState && (
        <div className=" absolute top-14 ">
          <section className=" w-[450px] h-auto rounded-2xl bg-white p-5">
            <hanko-profile />
          </section>
        </div>
      )}
    </>
  );
};

Это должно выглядеть так:

Теперь давайте воспользуемся @teamhanko/hanko-elements для управления выходами пользователей, создав компонент кнопки выхода. Создайте файл components/Logout.tsx и используйте в качестве его содержимого следующее:

"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<Hanko>();

  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 (
    <>
      <button type="button" onClick={logout}>Logout</button>
    </>
  );
};

Когда пользователь выходит из системы, запускается определенное событие, на которое вы можете подписаться, например перенаправление на определенную страницу после выхода из системы:

 const renewSession = useCallback(() => {
    router.replace("/");
  }, [router]);

  useEffect(
    () =>
      hanko?.onSessionExpired(() => {
        renewSession();
      }),

    [hanko, renewSession]
  );

Мы будем использовать обе кнопки в левом верхнем углу нашей страницы Todo.

ℹ️Для получения дополнительной информации обо всех событиях, которые вы можете «прослушать» из клиента Hanko, нажмите здесь.

Настройка компонентов Hanko

Компоненты Hanko очень легко настроить! Чтобы изменить компонент <hanko-auth>, внутри нашего файла globals.css мы можем изменить значения по умолчанию предварительно настроенных переменных CSS, перечисленных здесь, чтобы изменить цвет по умолчанию для основной кнопки и радиус границы, например:

:root {
  --border-radius: 20px;
  --brand-color: #ff2e4c;
  --brand-color-shade-1: #d52845;
  --brand-color-shade-2: #d62a4c;
}

Для получения дополнительной информации о настройке компонентов Hanko нажмите здесь.

Проверка JWT с помощью библиотеки jose

JWT подписан Hanko, и для защиты нашего приложения нам все равно необходимо проверить JWT.

Что такое JWT?Веб-токен JSON (JWT) — это компактный и автономный способ безопасной передачи информации между сторонами в виде объекта JSON. Цель JWT — обеспечить подлинность данных.

Ханко занимается аутентификацией и подписанием JWT. При успешной аутентификации с помощью Hanko устанавливается файл cookie, который содержит указанный JWT в качестве значения. На самом деле нам не нужно много знать о них, но стоит ознакомиться с частями JWT (заголовок, полезные данные и подпись), а также с тем, что такое JWKS. Для получения дополнительной информации вы также можете посетить JWT.io.

Чтобы проверить JWT, нам нужно установить пакет jose-jwt:

npm i jose

Jose — это модуль JavaScript, который поддерживает JWT и предоставляет функции подписи и проверки токенов.

Для получения дополнительной информации о Хосе нажмите здесь.

Промежуточное ПО

Создайте новый файл middleware.tsx в корне вашего проекта и используйте следующий код:

import * as jose from "jose";
import { NextRequest, NextResponse } 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));
}
}

Для проверки JWT нам нужен токен и JWKS. Мы получаем токен из файла cookie «hanko», а затем получаем набор веб-ключей JSON (JWKS), вызывая функцию createRemoteJWKSet из jose. Затем мы вызываем await jose.jwtVerify(token, JWKS). Если токен можно проверить, то обещание, возвращаемое функцией, преобразуется в декодированный токен. Если это невозможно проверить, то обещание отклоняется, и мы можем поймать ошибку и обработать ее соответствующим образом, например. путем перенаправления пользователя на страницу входа/домашнюю страницу. Если вы console.log const VerifiedJWT, вы должны увидеть декодированный токен, показывающий полезную нагрузку, protectedHeader и ключ. Внутри ключа вы должны увидеть «истину», если он проверен.

Для получения дополнительной информации о промежуточном программном обеспечении Next.js нажмите здесь.

Защита приложения и перенаправление 🔐

Мы хотим предотвратить получение неавторизованными пользователями доступа к личным данным пользователя. Простой способ сделать это — добавить защищаемые пути в конфигурацию промежуточного программного обеспечения. Скопируйте следующий код в конец файла middleware.tsx:

export const config = {
  matcher: ["/todo"],
};

Обновите страницу входа, чтобы подписаться на события клиента Hanko и перенаправить на страницу Todo после успешного входа в систему:

"use client";
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { register, Hanko } from "@teamhanko/hanko-elements";

const hankoApi = "YOUR_HANKO_API_URL";
export default function Login() {
  const router = useRouter();
  const [hanko, setHanko] = useState<Hanko>();

  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 (
    <div className="flex min-h-screen justify-center items-center bg-slate-50">
      <div className="bg-white p-5 rounded-2xl shadow-md">
        <hanko-auth />
      </div>
    </div>
  );
}

Время отображать правильные задачи ✨

Наконец, мы должны отображать задачи только для пользователя, вошедшего в систему. Для этого нам нужно связать задачи с правильным «идентификатором пользователя». Первым шагом является обновление модели Todo в Prisma schema:

model Todo {
  userId String
  id String @id @default(uuid())
  title String
  complete Boolean
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Затем выполните следующую команду, чтобы создать миграцию:

npx prisma migrate

Или выполните следующее, чтобы отправить изменения схемы непосредственно в базу данных:

npx prisma db push

Следующий шаг — обновить файл api/todo/route.ts, чтобы получить идентификатор пользователя из токена, а затем создать новую задачу, если идентификатор пользователя существует:

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 });
  }
}

Последний шаг — обновить вызов Prisma, чтобы получить все задачи из todo/page.tsx:

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 (
    <main className=" flex flex-col min-h-screen justify-center items-center bg-slate-50 relative ">
      <div className="absolute top-4 left-16">
        <div className=" relative py-4 space-x-6">
          <Profile />
          <Logout />
        </div>
      </div>
      <div className="bg-slate-300 rounded-3xl py-6  h-[400px] w-[450px] flex flex-col text-slate-800">
        <h1 className="text-3xl text-center">My to dos</h1>
        <NewTodo />
        <ul className="px-6">
          <TodoItem todos={todos} />
        </ul>
      </div>
    </main>
  );
}

🤩 Демонстрация приложения

Ты сделал это! 🎉

Поздравляем! Теперь у вас есть полнофункциональное приложение Todo!

Спасибо, что дошли до конца, сегодня вы узнали, как реализовать некоторые новые функции Next.js, как аутентифицировать пользователей с помощью Hanko и немного о базах данных с помощью Prisma.

Next.js App Router, безусловно, стал для меня испытанием: переход к изучению того, когда и как использовать некоторые новые функции, может занять время, но, с моей точки зрения, попробовать стоит.

Во времена постоянных изменений возможность рассчитывать на простую в интеграции аутентификацию становится почти мечтой. Hanko стремится изменить способ взаимодействия разработчиков и пользователей с обеими сторонами входа в систему.

Спасибо за чтение, auf wiedersehen! 👋

Источник:

#NestJS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу