Перейти к содержанию

Вход, сессия и профиль Clerk

Оглавление курса · Скачать все состояния проекта.

Учебная задача: Получить доверенную личность на сервере и защищать рабочие маршруты.

Входное состояние: Рабочие данные находятся в локальной PostgreSQL-базе. Выходное состояние: Рабочие страницы требуют входа; сервер получает userId, интерфейс показывает профиль. Продолжаем «Смену» — учебную диспетчерскую заявок мастерской. Нужны основы JavaScript, TypeScript, React и HTTP. Используем App Router, Next 16.4.0, React 19.3.0 и Node.js не ниже 20.9.0. Основные версии закреплены; диапазоны остальных пакетов ещё не разрешены установкой и lockfile отсутствует. Полный каталог lesson-14 в архиве соответствует окончанию этой главы. Выбирайте один снимок целиком: перенос отдельных файлов из будущих этапов меняет его зависимости и правила данных.

Исходники подготовлены редакционным чтением. Пакеты, сервер, сборка, миграции, примеры и внешнее размещение не выполнялись. Ответы страницы и изменения данных ниже являются ожидаемыми, example_execution: not_run.

До сих пор сервер не различал сотрудников. Теперь запрос должен иметь доверенную личность, иначе запись «владелец заявки» останется простой произвольной строкой. Подключим Clerk и отделим три задачи: интерфейс входа, предварительную защиту рабочих путей и проверку личности внутри серверного слоя. В следующем уроке эта личность станет основанием ограничения конкретных строк и действий.

Используем SDK @clerk/nextjs ветки 7, соответствующий Core 3. В этой версии условный интерфейс строится через Show; старые SignedIn, SignedOut и Protect нельзя переносить без обновления. Основные компоненты сохраняют ясные роли: ClerkProvider подключает контекст, SignIn показывает вход, SignUp регистрацию, UserButton доступ к профилю и выходу. Установка SDK и создание внешней конфигурации не выполнялись.

Ключи и маршруты

В .env.example указаны пустые места для публичного и секретного ключей. Publishable key предназначен для клиентского взаимодействия, secret key остаётся на сервере. Их названия не делают любой ключ взаимозаменяемым. Значения должны относиться к выбранной среде Clerk; случайное смешение разработки и рабочего домена даст другой договор сессий. В скачиваемом архиве нет действующих реквизитов.

Страницы входа и регистрации используют необязательные catch-all сегменты. Готовый компонент может иметь внутренние шаги с дополнительными частями пути, поэтому одна точная страница без такого договора могла бы не обслужить весь переход. Переменные URL явно связывают SDK с этими страницами. Fallback redirect приводит к списку, если у процесса нет другого назначенного адреса возвращения.

ClerkProvider помещён внутри body корневой компоновки. Он окружает страницы, сохраняя корневые html и body на своём уровне. Это соответствует текущей рекомендации и оставляет пространство для современного режима построения. Не переносим весь root в Client Component ради провайдера: Next и React позволяют серверному родителю включить клиентскую инфраструктуру через композицию.

Условный интерфейс и действительная защита

AuthControls показывает кнопку входа при when="signed-out" и UserButton при when="signed-in". Такая условность делает шапку понятной. Но Show визуально управляет содержимым и не является достаточным способом скрыть чувствительные данные, уже переданные клиенту. Рабочую заявку нужно ограничить на сервере до построения интерфейса. Поэтому в компоненты контроля не вкладываем секретные поля в надежде, что условие их защитит.

Proxy теперь использует clerkMiddleware. Сама функция не закрывает все пути автоматически. Внутри явно проверяются /tickets и его дочерние сегменты, затем вызывается auth.protect(). Конфигурация matcher включает необходимые страницы и API. Публичная главная страница, руководство и учебный каталог сохраняют собственную роль; наличие обработки Clerk не делает их все персональными автоматически.

Предварительная защита помогает направить анонимного посетителя ко входу. Однако прямое обращение к серверному действию и конкретному объекту требует отдельной проверки. Поэтому requireActor вызывает асинхронный auth() в серверном модуле и убеждается в наличии userId. При его отсутствии выполняется перенаправление ко входу. Слой данных вызывает эту функцию независимо от удобного пути обычной навигации.

Сессия не является полем формы

userId получен из доверенного механизма сессии, а не из hidden input или параметра URL. Клиент может прислать строку с любым именем, но она не должна назначать владельца. Этот принцип понадобится форме создания. Поле ownerId модели будет заполнено сервером на основании текущей личности, а не выбранного посетителем текста.

В этом промежуточном снимке все вошедшие пользователи ещё получают общий локальный список. Это обязательное честное ограничение главы: аутентификация установлена, предметная авторизация появится следующим шагом. Не используйте этот этап с настоящими личными обращениями. Сам факт входа не доказывает право прочитать любую запись, и auth.protect() не добавляет условие владельца в SQL автоматически.

Готовый профиль позволяет пользователю управлять предусмотренными настройками и выйти. Политика срока сессий, доступных способов входа и регистрации определяется внешней конфигурацией Clerk. Архив не сообщает, что парольless-вход или недельный срок уже включены. Если мастерская требует такие правила, их нужно осмысленно настроить в своей среде и подтвердить отдельно. Интерфейс SDK не выбирает бизнес-политику за владельца проекта.

Разбор ожидаемого пути

Анонимный посетитель открывает /tickets: предварительная и серверная проверки требуют входа. После успешной самостоятельной конфигурации и входа сервер получает userId. UserButton отражает действующее состояние интерфейса. При выходе новые рабочие запросы снова должны потребовать личность. Уже отправленные клиенту данные не исчезают из истории браузера магически, поэтому дальнейшая проверка важна для каждого нового чтения.

Для самостоятельного разбора проследите, где живёт секретный ключ, где выбирается защищённый путь и где сервер подтверждает личность. Затем объясните, почему демонстрационный demo-author не становится владельцем текущего пользователя сам. Следующая глава соединит идентификатор, роль и условие выборки, чтобы вход приобрёл конкретное предметное полномочие.

Проверьте смысл границ

Состояние входа можно показать на публичной странице, не открывая персональные данные. Кнопка профиля описывает текущий интерфейс, а requireActor задаёт условие серверной операции. Если сессия исчезла между отображением формы и отправкой, действие должно оценить новое состояние, а не верить старой картинке шапки.

Внешний профиль и строка PostgreSQL имеют разные жизненные циклы. Выход не удаляет заявки, а удаление учётной записи не реализовано как каскад базы. Такой процесс потребовал бы отдельного решения о владельцах и истории. На текущем шаге связываем только действующую личность запроса с будущими предметными операциями.

Полные файлы этого шага

Следующие файлы показаны полностью. Остальные согласованные компоненты и настройки находятся в том же каталоге архива; сохраняйте их вместе при самостоятельном прохождении.

app/layout.tsx

import "./globals.css";
import { ClerkProvider } from "@clerk/nextjs";
import type { Metadata } from "next";
import type { ReactNode } from "react";
export const metadata: Metadata = { title: { default: "Смена", template: "%s | Смена" }, description: "Учебная диспетчерская мастерской" };
export default function RootLayout({ children, modal }: { children: ReactNode; modal: ReactNode }) {
  return <html lang="ru"><body><ClerkProvider>{children}{modal}</ClerkProvider></body></html>;
}

components/auth-controls.tsx

import { Show, SignInButton, UserButton } from "@clerk/nextjs";
export function AuthControls() {
  return <><Show when="signed-out"><SignInButton><button>Войти</button></SignInButton></Show><Show when="signed-in"><UserButton /></Show></>;
}

proxy.ts

import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware(async (auth, request) => {
  if (request.nextUrl.pathname === "/tickets" || request.nextUrl.pathname.startsWith("/tickets/")) await auth.protect();
});
export const config = { matcher: ["/", "/tickets/:path*", "/sign-in/:path*", "/sign-up/:path*", "/api/:path*", "/guide/:path*", "/catalog/:path*"] };

lib/actor.ts

import "server-only";
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
export async function requireActor() {
  const { userId } = await auth();
  if (!userId) redirect("/sign-in");
  return { userId };
}

app/sign-in/[[...sign-in]]/page.tsx

import { SignIn } from "@clerk/nextjs";
export default function SignInPage() { return <main><h1>Вход в «Смену»</h1><SignIn /></main>; }

.env.example

DATABASE_URL=
# Нужна собственная локальная база PostgreSQL. Секрет не добавляют в NEXT_PUBLIC_.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/tickets
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/tickets

Документация и исходники

Договор этой главы раскрыт в официальной документации. Сопоставьте её API с выбранной версией и с объяснённым источником каждого значения. Архив «Смены» содержит двадцать полных состояний и условия самостоятельной подготовки. Ожидаемый результат оценивается по текущей главе: поздние проверки владельца, база и внешние ключи не являются неявно выполненными предпосылками раннего учебного состояния.