refactor: migrate authentication from Supabase to Better Auth and update project configuration

This commit is contained in:
Jose Selesan
2026-04-17 08:35:54 -03:00
parent aabd9a8266
commit b3f1b72da4
14 changed files with 87 additions and 332 deletions

View File

@@ -1,9 +1,5 @@
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Fallbacks accepted too:
# VITE_SUPABASE_URL=https://your-project-ref.supabase.co
# VITE_SUPABASE_ANON_KEY=your-anon-key
BETTER_AUTH_SECRET=your-random-secret
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
LOG_LEVEL=debug
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/monorepo

View File

@@ -1,11 +1 @@
// model User {
// id String @id @db.Uuid
// supabaseUserId String @unique @map("supabase_user_id") @db.Uuid
// email String @unique
// fullName String @map("full_name")
// createdAt DateTime @default(now()) @map("created_at")
// updatedAt DateTime @updatedAt @map("updated_at")
// complexes ComplexUser[]
// @@map("users")
// }
// User model is defined in auth.prisma (Better Auth schema).

View File

@@ -1,26 +0,0 @@
import { createClient } from '@supabase/supabase-js';
let cachedClient: ReturnType<typeof createClient> | null = null;
export function getSupabaseAdminClient() {
if (cachedClient) return cachedClient;
const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL;
const supabaseServiceRoleKey = Bun.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error(
'Missing Supabase admin env vars. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in backend environment.'
);
}
cachedClient = createClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
});
return cachedClient;
}

View File

@@ -1,18 +0,0 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL;
const supabaseAnonKey = Bun.env.SUPABASE_ANON_KEY ?? Bun.env.VITE_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
'Missing Supabase env vars. Set SUPABASE_URL/SUPABASE_ANON_KEY (or VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY) in backend environment.'
);
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
});

View File

@@ -2,14 +2,12 @@ import { createHash, randomInt } from 'node:crypto';
import { auth } from '@/lib/auth';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import { getSupabaseAdminClient } from '@/lib/supabase-admin';
import type {
OnboardingCompleteInput,
OnboardingResendOtpInput,
OnboardingStartInput,
OnboardingVerifyOtpInput,
} from '@repo/api-contract';
import type { User as SupabaseAuthUser } from '@supabase/supabase-js';
import { v7 as uuidv7 } from 'uuid';
const OTP_LENGTH = 6;
@@ -126,77 +124,6 @@ async function sendOtpEmail(email: string, otpCode: string) {
});
}
async function findSupabaseUserByEmail(email: string): Promise<SupabaseAuthUser | null> {
let page = 1;
const perPage = 100;
while (page <= 10) {
const { data, error } = await getSupabaseAdminClient().auth.admin.listUsers({
page,
perPage,
});
if (error) {
throw new Error(`No se pudo consultar usuarios en Supabase: ${error.message}`);
}
const found = data.users.find((user) => user.email?.toLowerCase() === email.toLowerCase());
if (found) {
return found;
}
if (data.users.length < perPage) {
return null;
}
page += 1;
}
return null;
}
async function ensureSupabaseUser(params: {
email: string;
fullName: string;
password: string;
}): Promise<string> {
const existingUser = await findSupabaseUserByEmail(params.email);
if (existingUser) {
const { error } = await getSupabaseAdminClient().auth.admin.updateUserById(existingUser.id, {
password: params.password,
email_confirm: true,
user_metadata: {
full_name: params.fullName,
name: params.fullName,
},
});
if (error) {
throw new Error(`No se pudo actualizar usuario existente en Supabase: ${error.message}`);
}
return existingUser.id;
}
const { data, error } = await getSupabaseAdminClient().auth.admin.createUser({
email: params.email,
password: params.password,
email_confirm: true,
user_metadata: {
full_name: params.fullName,
name: params.fullName,
},
});
if (error || !data.user) {
throw new Error(`No se pudo crear usuario en Supabase: ${error?.message ?? 'unknown error'}`);
}
return data.user.id;
}
export async function startOnboarding(input: OnboardingStartInput): Promise<StartOnboardingResult> {
const email = normalizeEmail(input.email);
const otpCode = createOtpCode();

View File

@@ -1,7 +1,7 @@
import { createHash, randomInt } from 'node:crypto';
import { auth } from '@/lib/auth';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import { getSupabaseAdminClient } from '@/lib/supabase-admin';
import type {
PasswordResetResendInput,
PasswordResetStartInput,
@@ -189,34 +189,17 @@ async function sendPasswordChangedEmail(
});
}
async function findSupabaseUserByEmail(email: string): Promise<{ id: string } | null> {
let page = 1;
const perPage = 100;
async function findBetterAuthUserByEmail(email: string): Promise<{ id: string } | null> {
const user = await db.user.findUnique({
where: { email },
select: { id: true },
});
while (page <= 10) {
const { data, error } = await getSupabaseAdminClient().auth.admin.listUsers({
page,
perPage,
});
if (error) {
throw new Error(`No se pudo consultar usuarios en Supabase: ${error.message}`);
}
const found = data.users.find((user) => user.email?.toLowerCase() === email.toLowerCase());
if (found) {
return { id: found.id };
}
if (data.users.length < perPage) {
return null;
}
page += 1;
if (!user) {
return null;
}
return null;
return { id: user.id };
}
export async function startPasswordReset(
@@ -224,7 +207,7 @@ export async function startPasswordReset(
): Promise<PasswordResetStartResult> {
const email = normalizeEmail(input.email);
const existingUser = await findSupabaseUserByEmail(email);
const existingUser = await findBetterAuthUserByEmail(email);
if (!existingUser) {
return {
message: 'Si el email existe, recibirás un código de verificación.',
@@ -369,13 +352,30 @@ export async function verifyOtpAndResetPassword(
throw new PasswordResetError('Código incorrecto.', 400);
}
const existingUser = await findSupabaseUserByEmail(request.email);
const existingUser = await findBetterAuthUserByEmail(request.email);
if (!existingUser) {
throw new PasswordResetError('Usuario no encontrado.', 404);
}
await getSupabaseAdminClient().auth.admin.updateUserById(existingUser.id, {
password: newPassword,
const resetToken = uuidv7();
const identifier = `reset-password:${resetToken}`;
const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
await db.verification.create({
data: {
id: uuidv7(),
identifier,
value: existingUser.id,
expiresAt,
},
});
await auth.api.resetPassword({
asResponse: false,
body: {
token: resetToken,
newPassword,
},
});
await db.passwordResetRequest.update({

View File

@@ -1,3 +0,0 @@
import type { User } from '@supabase/supabase-js';
export type AuthUser = User;