Supabase поддерживает авторизацию пользователей с помощью адреса электронной почты и пароля, но как мне разрешить пользователям входить в систему с использованием имени пользователя (вместо адреса электронной почты) и пароля?
Библиотека SupabaseClient позволяет зарегистрироваться и войти в систему, используя адрес электронной почты, но не предоставляет возможности даже иметь имя пользователя.
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password',
});





Посмотрев повсюду, я нашел кусочки решения в нескольких местах, но не подходящее решение. Поэтому я решил написать это подробное решение, которое, надеюсь, будет полезно другим разработчикам с таким же вариантом использования.
Самое близкое решение, которое я нашел, было в обсуждении на GitHub. Это решение имело правильную идею, но оно ошибочно, поскольку адрес электронной почты каждого пользователя будет раскрыт.
Я придумал два подхода:
public.users, содержащую только имена пользователей, со ссылкой на внешний ключ к таблице auth.users. Эта настройка гарантирует, что электронные письма не могут быть получены с помощью ANON_KEY. Затем вам нужно будет реализовать конечную точку на стороне сервера, которая принимает имя пользователя и пароль, получает электронное письмо с помощью SERVICE_ROLE_KEY, использует это электронное письмо для аутентификации пользователя, и все это на стороне сервера.Предупреждение:
SERVICE_ROLE_KEYникогда не должен быть раскрыт, поэтому эта логика должна быть только на стороне сервера.
Примечание. Лично я выбрал подход, использующий функцию Dyno. Причина этого в том, что я предпочитаю хранить код, связанный с Supabase, в Supabase, а не в своем внешнем коде (я использую структуру SSR).
Первый шаг — создать таблицу, которую мы собираемся сделать доступной, а также несколько триггеров для ее синхронизации с 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не позволит добавить повторяющееся имя пользователя, что приведет к сбою триггера, а также процесса регистрации).
Функция 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' })
Вот и все, теперь ваш клиент должен быть безопасно авторизован!