Auth.js (формально NextAuth.js) — это фантастическая библиотека для настройки аутентификации в вашем приложении на основе Node.js.

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

Auth.js предпочитает, чтобы ваша база данных была настроена очень особым образом. Итак, что произойдет, если вам нужно сделать что-то другое? Причин несколько:

  • вы предпочитаете, чтобы ваши таблицы базы данных назывались по-другому, например: users вместо user,
  • разные имена столбцов, например: user_id вместо userId,
  • это входит в уже существующий проект, поэтому изменение первичных и внешних ключей невозможно,
  • вы настороженно относитесь к безопасности и хотите поставить несколько ловушек,
  • есть дополнительные данные, которые вы хотите сохранить, или
  • вы хотите/должны использовать очень специфический движок базы данных, который не поддерживается.

К счастью, есть способ заставить Auth.js работать по-вашему.

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

Где вы создаете свой адаптер в своем проекте, зависит от вас, но Auth.js должен использовать его при инициализации, и это зависит от вашей настройки. Вот несколько ссылок для начала:

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

import Email from "@auth/core/providers/email";
import { PrismaClient } from "@prisma/client";

import { MyAdapter } from "../../Adapter";

export default NextAuth({
  adapter: MyAdapter(PrismaClient), //  <-- db client goes in the adapter
  providers: [
    Email({
      ...
    }),
  ],
})

Говоря о Prisma, вот схема для трех таблиц, которые были созданы в базе данных до того, как они были вытащены. Вы могли заметить, что я предпочитаю, чтобы мои внешние ключи использовали имя таблицы в единственном числе с добавлением _id (и метки времени с добавлением _at), что отличается от стандарта Auth.js:

model users {
  id          BigInt     @id @default(autoincrement()) @db.UnsignedBigInt
  name        String     @db.VarChar(255)
  email       String     @unique(map: "email") @db.VarChar(255)
  created_at  DateTime   @default(now()) @db.Timestamp(0)

  sessions    sessions[]
}

model sessions {
  id         BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  user_id    BigInt    @db.UnsignedBigInt
  token      String    @unique(map: "token") @db.VarChar(255)
  expires_at DateTime? @db.Timestamp(0)
  users      users     @relation(fields: [user_id], references: [id], onDelete: Cascade, map: "sessions_ibfk_1")

  @@index([user_id], map: "user_id")
}

model tokens {
  identifier String    @db.VarChar(255)
  token      String    @unique(map: "token") @db.VarChar(255)
  expires_at DateTime? @db.Timestamp(0)

  @@unique([identifier, token], map: "identifier_token")
}

Следите за этим составным ключом identifier_token, я воспользуюсь им позже.

Золотое правило

При работе с адаптером следует помнить одно важное правило: входные и выходные данные принадлежат Auth.js, но запросы к базе данных ваши.

Вот базовый адаптер для Auth.js (и вам следует заменить MyAdapter на что-то другое):

export default function MyAdapter(client) {
  return {
    async createUser(user) {
      // Create a new record in your user's table with the values in the user object
      return user
    },
    async getUser(id) {
      // Get the user with this id
      return user
    },
    async getUserByEmail(email) {
      // Get the user with this email
      return user
    },
    async updateUser({ id, ...data }) {
      // Update the user's data by their id
      return { id, ...data }
    },
    async deleteUser(id) {
       // Delete the user by their id
      return
    },
    async createSession({ sessionToken, userId, expires }) {
      // Create a new session record
      return { sessionToken, userId, expires }
    },
    async getSessionAndUser(sessionToken) {
      // Get the user and session token
      return { user, session }
    },
    async updateSession({ sessionToken, expires }) {
      // Update the session token
      return { sessionToken, expires }
    },
    async deleteSession(sessionToken) {
      // Delete the session token
      return
    },
    async createVerificationToken({ identifier, expires, token }) {
      // Create a new verification token
      return { identifier, expires, token }
    },
    async useVerificationToken({ identifier, token }) {
      // Delete the verification token (so it can't be used again)
      // For magic links this will called by the URL in the email
      return { identifier, token, expires }
    },
  }
}

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

Ранее я указал на identifier_token в схеме, которая является составным ключом между столбцами identifier и token в таблице. Хотя ваш дизайн может отличаться, это отличный способ показать, что правило их против вашего для useVerificationToken:

Мы начинаем с пустой асинхронной функции, у которой есть запрошенные входные и выходные данные:

async function useVerificationToken ({ identifier, token }) { // <-- theirs
  return { identifier, token, expires } // <-- also theirs
}

Когда вы добавляете вашзапрос к базе данных, вы можете использовать реквизиты Auth.js, но убедитесь, что вы возвращаете правильные пары ключ/значение:

async function useVerificationToken ({ identifier, token }) {
  const verificationToken = await client.tokens.delete({
    where: {
      identifier_token: { // <-- compound key
        identifier,
        token,
      },
    },
  });
  
  return {
    identifier,
    token,
    expires: verificationToken.expires_at // <-- their key, your value
  };
}

Я использую expires_at в своей базе данных, но Auth.js хочет, чтобы expires возвращалось в объекте, поэтому мне нужно вернуть его вместе с identifier и token.

Я также должен обрабатывать identifier_token как объект в моем запросе, но это то, что я получаю при использовании составного ключа.

Заканчивать

Все функции в Auth.js работают вместе, поэтому, если вы хотите настроить работу адаптера, независимо от ваших причин, вам нужно поддерживать Auth.js в хорошем состоянии и возвращать ожидаемые ключи. Что происходит между запуском каждой функции и оператором return, зависит от вас.

И вы всегда можете ознакомиться с другими пакетами адаптеров Auth.js и изучить различные способы создания собственного.