Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Вход и выход пользователя

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

Проверка пароля

Создайте файл app/auth.server.ts. Пароли хранят только в виде хеша; здесь для этого используется встроенный модуль node:crypto:

// app/auth.server.ts
import { scryptSync, timingSafeEqual } from "node:crypto";
import { redirect } from "react-router";
import { getSession } from "./sessions.server";
 
type User = { id: string; email: string; passwordHash: string; salt: string };
 
// В реальном проекте пользователи лежат в таблице users.
const users: User[] = [];
 
export function hashPassword(password: string, salt: string) {
  return scryptSync(password, salt, 32).toString("hex");
}
 
export async function verifyLogin(email: string, password: string) {
  const user = users.find((u) => u.email === email);
  if (!user) return null;
  const hash = Buffer.from(hashPassword(password, user.salt), "hex");
  return timingSafeEqual(hash, Buffer.from(user.passwordHash, "hex")) ? user : null;
}
 
export async function requireUserId(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");
  if (!userId) throw redirect("/login");
  return userId;
}

requireUserId либо возвращает идентификатор, либо выбрасывает перенаправление на /login, и выполнение загрузчика на этом заканчивается.

Страница входа

Добавьте маршрут route("login", "routes/login.tsx"):

// app/routes/login.tsx
import { Form, data, redirect } from "react-router";
import type { Route } from "./+types/login";
import { verifyLogin } from "../auth.server";
import { commitSession, getSession } from "../sessions.server";
 
export async function action({ request }: Route.ActionArgs) {
  const form = await request.formData();
  const user = await verifyLogin(String(form.get("email")), String(form.get("password")));
  if (!user) {
    return data({ error: "Неверный email или пароль" }, { status: 400 });
  }
 
  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", user.id);
  return redirect("/", { headers: { "Set-Cookie": await commitSession(session) } });
}
 
export default function Login({ actionData }: Route.ComponentProps) {
  return (
    <Form method="post">
      <input name="email" type="email" placeholder="anna@example.com" />
      <input name="password" type="password" />
      {actionData?.error && <p>{actionData.error}</p>}
      <button type="submit">Войти</button>
    </Form>
  );
}

При верных данных пользователь попадёт на главную уже вошедшим, при неверных увидит сообщение об ошибке.

Защита страницы

В загрузчике и действии app/routes/new-post.tsx первой строкой вызовите проверку:

// app/routes/new-post.tsx
import { requireUserId } from "../auth.server";
 
export async function loader({ request }: Route.LoaderArgs) {
  await requireUserId(request);
  return null;
}

Гость, открывший /posts/new, будет перенаправлен на /login. Ту же строку добавьте в начало action.

Выход

Выход — действие, которое удаляет сессию. Маршрут route("logout", "routes/logout.tsx"):

// app/routes/logout.tsx
import { redirect } from "react-router";
import type { Route } from "./+types/logout";
import { destroySession, getSession } from "../sessions.server";
 
export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get("Cookie"));
  return redirect("/", { headers: { "Set-Cookie": await destroySession(session) } });
}

В шапке разместите форму: Form method="post" action="/logout" с кнопкой «Выйти». Выход делают через POST, а не ссылкой, чтобы его не вызвала предзагрузка или чужая картинка.

Частая ошибка

Защитить только loader, а action оставить открытым. Скрыть форму от гостя мало: запрос POST на /posts/new можно отправить и без формы. Вызывайте requireUserId в каждом action, который меняет данные.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.