Как авторизовать пользователей с именем пользователя в Supabase?

Supabase поддерживает авторизацию пользователей с помощью адреса электронной почты и пароля, но как мне разрешить пользователям входить в систему с использованием имени пользователя (вместо адреса электронной почты) и пароля?

Библиотека SupabaseClient позволяет зарегистрироваться и войти в систему, используя адрес электронной почты, но не предоставляет возможности даже иметь имя пользователя.

const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'password',
});
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
350
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Объяснение

Самое близкое решение, которое я нашел, было в обсуждении на GitHub. Это решение имело правильную идею, но оно ошибочно, поскольку адрес электронной почты каждого пользователя будет раскрыт.

Я придумал два подхода:

  1. Серверный запрос с SERVICE_KEY: создайте таблицу public.users, содержащую только имена пользователей, со ссылкой на внешний ключ к таблице auth.users. Эта настройка гарантирует, что электронные письма не могут быть получены с помощью ANON_KEY. Затем вам нужно будет реализовать конечную точку на стороне сервера, которая принимает имя пользователя и пароль, получает электронное письмо с помощью SERVICE_ROLE_KEY, использует это электронное письмо для аутентификации пользователя, и все это на стороне сервера.
  2. Функция Deno Edge: используйте функцию Deno Edge для выполнения той же задачи, что и при подходе на стороне сервера. Этот метод особенно полезен, если вы не можете выполнить код на стороне сервера напрямую.

Предупреждение: SERVICE_ROLE_KEY никогда не должен быть раскрыт, поэтому эта логика должна быть только на стороне сервера.

Примечание. Лично я выбрал подход, использующий функцию Dyno. Причина этого в том, что я предпочитаю хранить код, связанный с Supabase, в Supabase, а не в своем внешнем коде (я использую структуру SSR).

Процедура

Шаг 1. Настройка PostgreSQL

Первый шаг — создать таблицу, которую мы собираемся сделать доступной, а также несколько триггеров для ее синхронизации с auth.users.

Создание таблицы public.profiles.

CREATE TABLE "public"."profiles" (
  "user_id" uuid PRIMARY KEY NOT NULL REFERENCES "auth"."users" ON DELETE CASCADE,
  "username" text UNIQUE NOT NULL
);

Включите безопасность на уровне строк. Это необходимо, чтобы анонимные пользователи не могли изменить эту таблицу.

ALTER TABLE "public"."profiles" ENABLE ROW LEVEL SECURITY;

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

CREATE POLICY "Enable read access for all users" ON "public"."profiles" FOR SELECT TO public USING (true);

Откуда же возьмутся эти имена пользователей, если пользователям не разрешено писать в таблицу? К счастью для нас, Supabase позволяет отправлять произвольные метаданные при регистрации пользователя. Мы просто включим имя пользователя в эти данные.

const { data, error } = await supabaseClient.auth.signUp({
  email: '[email protected]',
  password: 'password',
  options: {
    data: {
      username: 'username'
    }
  }
});

Когда пользователь будет создан, это имя пользователя будет сохранено в auth.users. Мы создадим триггер для чтения имени пользователя из этой таблицы при каждой вставке. См. документацию Supabase.

CREATE FUNCTION "public"."handle_new_user"()
RETURNS trigger
AS $pga$
BEGIN
  INSERT INTO public.profiles (user_id, username)
  VALUES (new.id, new.raw_user_meta_data ->> 'username');
  return new;
END;
$pga$
VOLATILE
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = '';
CREATE TRIGGER "after_user_created"
  AFTER INSERT ON "auth"."users"
  FOR EACH ROW
  EXECUTE PROCEDURE "public"."handle_new_user"();

Примечание. Вам также потребуется установить аналогичный триггер AFTER UPDATE для обновления записи public.profiles всякий раз, когда пользователь обновляет свой профиль.

Предупреждение. Убедитесь, что вы правильно тестируете триггеры, поскольку Supabase не позволит пользователю зарегистрироваться, если триггер не сработает. Это также полезно для нас, поскольку гарантирует, что никогда не будет повторяющихся имен пользователей (ограничение уникальности поля username не позволит добавить повторяющееся имя пользователя, что приведет к сбою триггера, а также процесса регистрации).

Шаг 2. Аутентификация на стороне сервера

Функция supabaseClient.auth.signInWithPassword() принимает только адрес электронной почты и пароль, поэтому нам просто нужно сначала получить электронное письмо, используя уникальное имя пользователя, и попробовать пройти аутентификацию с его помощью.

Для этого мы создадим функцию Deno Edge, которая принимает identifier (может быть либо именем пользователя, либо паролем) и password в качестве входных данных JSON (пример прокомментирован внизу кода ниже).

// This edge function handles user login requests.
// It accepts a JSON payload with an identifier (email or username) and password,
// retrieves the corresponding email if a username is provided, and attempts to sign in
// the user with Supabase authentication. It returns a JSON response indicating success
// or failure.

// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.

// Setup type definitions for built-in Supabase Runtime APIs
/// <reference types = "https://esm.sh/@supabase/functions-js/src/edge-runtime.d.ts" />
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);

Deno.serve(async (req) => {
  try {
    const { identifier, password } = await req.json();
    if (typeof identifier !== 'string' || typeof password !== 'string') {
      throw new Error('Invalid login credentials');
    }

    const email = await getEmail(identifier);
    if (!email) {
      throw new Error('Invalid login credentials');
    }

    const { data, error } = await supabase.auth.signInWithPassword({ email, password });
    if (error) {
      throw error;
    }

    return jsonResponse(data, 200);
  } catch (error) {
    return jsonResponse({ error: error.message }, error.status ?? 400);
  }
});

/**
 * Retrieve email by identifier (email or username).
 * @param {string} identifier - The email or username.
 * @returns {Promise<string | undefined>} The email address or undefined if not found.
 */
async function getEmail(identifier: string): Promise<string | undefined> {
  if (identifier.includes('@')) {
    return identifier;
  }

  const { data: profile } = await supabase
    .from('profiles')
    .select('user_id')
    .eq('username', identifier)
    .single();

  if (!profile?.user_id) {
    return undefined;
  }

  const { data: user } = await supabase.auth.admin.getUserById(profile.user_id);
  return user.user!.email;
}

/**
 * Helper function to create JSON responses.
 * @param {object} body - The response body.
 * @param {number} status - The HTTP status code.
 * @returns {Response} The HTTP response.
 */
function jsonResponse(body: object, status: number): Response {
  return new Response(JSON.stringify(body), {
    headers: { 'Content-Type': 'application/json' },
    status
  });
}

/* To invoke locally:

  1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
  2. Make an HTTP request:

  curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/login' \
    --header 'Content-Type: application/json' \
    --data '{"identifier":"username","password":"password"}'

*/

Примечание. Для этой функции вам придется отключить проверку JWT. См. здесь и здесь для получения дополнительной информации.

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

await supabaseClient.auth.setSession({ access_token: 'access_token', refresh_token: 'refresh_token' })

Вот и все, теперь ваш клиент должен быть безопасно авторизован!

Другие вопросы по теме