refactor: migrate authentication from Supabase to Better Auth and update project configuration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
export type AuthUser = User;
|
||||
Reference in New Issue
Block a user