refactor: integrate Better-Auth for authentication and remove Supabase dependencies

This commit is contained in:
Jose Selesan
2026-04-16 20:30:01 -03:00
parent f158845279
commit e5002ce440
29 changed files with 165 additions and 211 deletions

View File

@@ -8,6 +8,7 @@ model User {
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
role String @default("member")
complexes ComplexUser[]
@@unique([email])

View File

@@ -21,7 +21,7 @@ export function createApp() {
},
allowHeaders: ['Authorization', 'Content-Type'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
exposeHeaders: ["Content-Length"],
exposeHeaders: ['Content-Length'],
maxAge: 600,
credentials: true,
})

File diff suppressed because one or more lines are too long

View File

@@ -1499,7 +1499,8 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified',
image: 'image',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
updatedAt: 'updatedAt',
role: 'role'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]

View File

@@ -90,7 +90,8 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified',
image: 'image',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
updatedAt: 'updatedAt',
role: 'role'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]

View File

@@ -32,6 +32,7 @@ export type UserMinAggregateOutputType = {
image: string | null
createdAt: Date | null
updatedAt: Date | null
role: string | null
}
export type UserMaxAggregateOutputType = {
@@ -42,6 +43,7 @@ export type UserMaxAggregateOutputType = {
image: string | null
createdAt: Date | null
updatedAt: Date | null
role: string | null
}
export type UserCountAggregateOutputType = {
@@ -52,6 +54,7 @@ export type UserCountAggregateOutputType = {
image: number
createdAt: number
updatedAt: number
role: number
_all: number
}
@@ -64,6 +67,7 @@ export type UserMinAggregateInputType = {
image?: true
createdAt?: true
updatedAt?: true
role?: true
}
export type UserMaxAggregateInputType = {
@@ -74,6 +78,7 @@ export type UserMaxAggregateInputType = {
image?: true
createdAt?: true
updatedAt?: true
role?: true
}
export type UserCountAggregateInputType = {
@@ -84,6 +89,7 @@ export type UserCountAggregateInputType = {
image?: true
createdAt?: true
updatedAt?: true
role?: true
_all?: true
}
@@ -167,6 +173,7 @@ export type UserGroupByOutputType = {
image: string | null
createdAt: Date
updatedAt: Date
role: string
_count: UserCountAggregateOutputType | null
_min: UserMinAggregateOutputType | null
_max: UserMaxAggregateOutputType | null
@@ -198,6 +205,7 @@ export type UserWhereInput = {
image?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string
sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
complexes?: Prisma.ComplexUserListRelationFilter
@@ -211,6 +219,7 @@ export type UserOrderByWithRelationInput = {
image?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput
complexes?: Prisma.ComplexUserOrderByRelationAggregateInput
@@ -227,6 +236,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
image?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string
sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
complexes?: Prisma.ComplexUserListRelationFilter
@@ -240,6 +250,7 @@ export type UserOrderByWithAggregationInput = {
image?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
_count?: Prisma.UserCountOrderByAggregateInput
_max?: Prisma.UserMaxOrderByAggregateInput
_min?: Prisma.UserMinOrderByAggregateInput
@@ -256,6 +267,7 @@ export type UserScalarWhereWithAggregatesInput = {
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
role?: Prisma.StringWithAggregatesFilter<"User"> | string
}
export type UserCreateInput = {
@@ -266,6 +278,7 @@ export type UserCreateInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
@@ -279,6 +292,7 @@ export type UserUncheckedCreateInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
@@ -292,6 +306,7 @@ export type UserUpdateInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
@@ -305,6 +320,7 @@ export type UserUncheckedUpdateInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
@@ -318,6 +334,7 @@ export type UserCreateManyInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
}
export type UserUpdateManyMutationInput = {
@@ -328,6 +345,7 @@ export type UserUpdateManyMutationInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
}
export type UserUncheckedUpdateManyInput = {
@@ -338,6 +356,7 @@ export type UserUncheckedUpdateManyInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
}
export type UserCountOrderByAggregateInput = {
@@ -348,6 +367,7 @@ export type UserCountOrderByAggregateInput = {
image?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
}
export type UserMaxOrderByAggregateInput = {
@@ -358,6 +378,7 @@ export type UserMaxOrderByAggregateInput = {
image?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
}
export type UserMinOrderByAggregateInput = {
@@ -368,6 +389,7 @@ export type UserMinOrderByAggregateInput = {
image?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
}
export type UserScalarRelationFilter = {
@@ -441,6 +463,7 @@ export type UserCreateWithoutSessionsInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
}
@@ -453,6 +476,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
}
@@ -481,6 +505,7 @@ export type UserUpdateWithoutSessionsInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
}
@@ -493,6 +518,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
}
@@ -505,6 +531,7 @@ export type UserCreateWithoutAccountsInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserCreateNestedManyWithoutUserInput
}
@@ -517,6 +544,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
complexes?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutUserInput
}
@@ -545,6 +573,7 @@ export type UserUpdateWithoutAccountsInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUpdateManyWithoutUserNestedInput
}
@@ -557,6 +586,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
complexes?: Prisma.ComplexUserUncheckedUpdateManyWithoutUserNestedInput
}
@@ -569,6 +599,7 @@ export type UserCreateWithoutComplexesInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
}
@@ -581,6 +612,7 @@ export type UserUncheckedCreateWithoutComplexesInput = {
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
}
@@ -609,6 +641,7 @@ export type UserUpdateWithoutComplexesInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
}
@@ -621,6 +654,7 @@ export type UserUncheckedUpdateWithoutComplexesInput = {
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
}
@@ -682,6 +716,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
image?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
complexes?: boolean | Prisma.User$complexesArgs<ExtArgs>
@@ -696,6 +731,7 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
image?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
}, ExtArgs["result"]["user"]>
export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -706,6 +742,7 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
image?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
}, ExtArgs["result"]["user"]>
export type UserSelectScalar = {
@@ -716,9 +753,10 @@ export type UserSelectScalar = {
image?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
}
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
@@ -743,6 +781,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
image: string | null
createdAt: Date
updatedAt: Date
role: string
}, ExtArgs["result"]["user"]>
composites: {}
}
@@ -1176,6 +1215,7 @@ export interface UserFieldRefs {
readonly image: Prisma.FieldRef<"User", 'String'>
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
readonly role: Prisma.FieldRef<"User", 'String'>
}

View File

@@ -1,12 +1,12 @@
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
// If your Prisma file is located elsewhere, you can change the path
import { openAPI } from "better-auth/plugins";
import { db } from "./prisma";
import { openAPI } from 'better-auth/plugins';
import { db } from './prisma';
export const auth = betterAuth({
database: prismaAdapter(db, {
provider: "postgresql",
provider: 'postgresql',
}),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
@@ -18,9 +18,8 @@ export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
trustedOrigins: [process.env.APP_BASE_URL ?? "http://localhost:5173"],
plugins: [
],
trustedOrigins: [process.env.APP_BASE_URL ?? 'http://localhost:5173'],
plugins: [],
});
export type Session = typeof auth.$Infer.Session.session;

View File

@@ -1,27 +1,18 @@
import { createMiddleware } from "hono/factory";
import { auth } from "@/lib/auth";
import type { Session, User } from "@/lib/auth";
import { auth } from '@/lib/auth';
import type { AppEnv } from '@/types/hono';
import { createMiddleware } from 'hono/factory';
export type AuthEnv = {
Variables: {
user: User;
session: Session;
};
};
export const requireAuth = createMiddleware<AuthEnv>(async (c, next) => {
export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
console.log(session)
if (!session) {
return c.json({ message: "Unauthorized" }, 401);
return c.json({ message: 'Unauthorized' }, 401);
}
c.set("user", session.user);
c.set("session", session.session);
c.set('user', session.user);
c.set('session', session.session);
await next();
});

View File

@@ -11,9 +11,9 @@ function getSuperAdminEmails(): Set<string> {
}
export const requireSuperAdmin: MiddlewareHandler<AppEnv> = async (c, next) => {
const authUser = c.get('authUser');
const role = typeof authUser.app_metadata?.role === 'string' ? authUser.app_metadata.role : null;
const email = authUser.email?.toLowerCase();
const user = c.get('user');
const role = (user as any).role || 'member';
const email = user.email.toLowerCase();
if (role === 'super_admin') {
await next();

View File

@@ -10,10 +10,10 @@ type ComplexIdParams = { complexId: string };
export async function createAdminBookingHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateAdminBookingInput;
const appUserId = c.get('appUserId');
const user = c.get('user');
try {
const booking = await createAdminBooking(appUserId, complexId, payload);
const booking = await createAdminBooking(user.id, complexId, payload);
return c.json(booking, 201);
} catch (error) {
if (error instanceof AdminBookingServiceError) {

View File

@@ -10,10 +10,10 @@ type ComplexIdParams = { complexId: string };
export async function listAdminBookingsHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const query = c.req.valid('query' as never) as ListAdminBookingsQuery;
const appUserId = c.get('appUserId');
const user = c.get('user');
try {
const response = await listAdminBookings(appUserId, complexId, query);
const response = await listAdminBookings(user.id, complexId, query);
return c.json(response);
} catch (error) {
if (error instanceof AdminBookingServiceError) {

View File

@@ -10,10 +10,10 @@ type BookingIdParams = { id: string };
export async function updateAdminBookingStatusHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as BookingIdParams;
const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput;
const appUserId = c.get('appUserId');
const user = c.get('user');
try {
const booking = await updateAdminBookingStatus(appUserId, id, payload);
const booking = await updateAdminBookingStatus(user.id, id, payload);
return c.json(booking);
} catch (error) {
if (error instanceof AdminBookingServiceError) {

View File

@@ -126,12 +126,12 @@ function buildSlots(
return slots;
}
async function ensureComplexAccess(complexId: string, appUserId: string) {
async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId: appUserId,
userId,
},
},
include: {
@@ -205,11 +205,11 @@ function mapBookingResponse(booking: {
}
export async function listAdminBookings(
appUserId: string,
userId: string,
complexId: string,
query: ListAdminBookingsQuery
) {
await ensureComplexAccess(complexId, appUserId);
await ensureComplexAccess(complexId, userId);
const fromDate = parseIsoDate(query.fromDate);
const bookings = await db.courtBooking.findMany({
@@ -253,11 +253,11 @@ export async function listAdminBookings(
}
export async function createAdminBooking(
appUserId: string,
userId: string,
complexId: string,
input: CreateAdminBookingInput
) {
const complex = await ensureComplexAccess(complexId, appUserId);
const complex = await ensureComplexAccess(complexId, userId);
const bookingDate = parseIsoDate(input.date);
const dayOfWeek = getDayOfWeek(bookingDate);
@@ -436,7 +436,7 @@ export async function createAdminBooking(
}
export async function updateAdminBookingStatus(
appUserId: string,
userId: string,
bookingId: string,
input: UpdateAdminBookingStatusInput
) {
@@ -447,7 +447,7 @@ export async function updateAdminBookingStatus(
complex: {
users: {
some: {
userId: appUserId,
userId,
},
},
},

View File

@@ -1,9 +1,9 @@
import { Hono } from "hono";
import { auth } from "@/lib/auth";
import { AppEnv } from "@/types/hono";
import { auth } from '@/lib/auth';
import { AppEnv } from '@/types/hono';
import { Hono } from 'hono';
export const authRoutes = new Hono<AppEnv>();
authRoutes.on(["POST", "GET"], "/api/auth/*", (c) => {
authRoutes.on(['POST', 'GET'], '/api/auth/*', (c) => {
return auth.handler(c.req.raw);
});

View File

@@ -5,8 +5,8 @@ import type { CreateComplexInput } from '@repo/api-contract';
export async function createComplexHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as CreateComplexInput;
const authUser = c.get('authUser');
const adminEmail = authUser.email;
const user = c.get('user');
const adminEmail = user.email;
if (!adminEmail) {
return c.json({ message: 'El usuario autenticado no tiene email.' }, 400);

View File

@@ -2,7 +2,7 @@ import { listMyComplexes } from '@/modules/complex/services/complex.service';
import type { AppContext } from '@/types/hono';
export async function listMyComplexesHandler(c: AppContext) {
const appUserId = c.get('appUserId');
const complexes = await listMyComplexes(appUserId);
const user = c.get('user');
const complexes = await listMyComplexes(user.id);
return c.json(complexes);
}

View File

@@ -93,9 +93,9 @@ export async function getComplexBySlug(slug: string) {
});
}
export async function listMyComplexes(appUserId: string) {
export async function listMyComplexes(userId: string) {
const complexUsers = await db.complexUser.findMany({
where: { userId: appUserId },
where: { userId },
include: {
complex: true,
},

View File

@@ -7,10 +7,10 @@ type ComplexIdParams = { complexId: string };
export async function createCourtHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateCourtInput;
const appUserId = c.get('appUserId');
const user = c.get('user');
try {
const court = await createCourt(appUserId, complexId, payload);
const court = await createCourt(user.id, complexId, payload);
return c.json(court, 201);
} catch (error) {
if (error instanceof CourtServiceError) {

View File

@@ -5,10 +5,10 @@ type ComplexIdParams = { complexId: string };
export async function listCourtsByComplexHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const appUserId = c.get('appUserId');
const user = c.get('user');
try {
const courts = await listCourtsByComplex(complexId, appUserId);
const courts = await listCourtsByComplex(complexId, user.id);
return c.json(courts);
} catch (error) {
if (error instanceof CourtServiceError) {

View File

@@ -7,10 +7,10 @@ type CourtIdParams = { id: string };
export async function updateCourtHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as CourtIdParams;
const payload = c.req.valid('json' as never) as UpdateCourtInput;
const appUserId = c.get('appUserId');
const user = c.get('user');
try {
const court = await updateCourt(appUserId, id, payload);
const court = await updateCourt(user.id, id, payload);
return c.json(court);
} catch (error) {
if (error instanceof CourtServiceError) {

View File

@@ -61,12 +61,12 @@ function assertAvailabilityRanges(
}
}
async function ensureComplexAccess(complexId: string, appUserId: string) {
async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId: appUserId,
userId,
},
},
include: {
@@ -137,14 +137,14 @@ async function enforcePlanCourtLimit(complexId: string) {
}
}
async function getCourtByIdForUser(courtId: string, appUserId: string) {
async function getCourtByIdForUser(courtId: string, userId: string) {
return db.court.findFirst({
where: {
id: courtId,
complex: {
users: {
some: {
userId: appUserId,
userId,
},
},
},
@@ -195,8 +195,8 @@ function mapCourtResponse(court: CourtWithRelations) {
};
}
export async function listCourtsByComplex(complexId: string, appUserId: string) {
await ensureComplexAccess(complexId, appUserId);
export async function listCourtsByComplex(complexId: string, userId: string) {
await ensureComplexAccess(complexId, userId);
const courts = await db.court.findMany({
where: { complexId },
@@ -218,8 +218,8 @@ export async function listCourtsByComplex(complexId: string, appUserId: string)
return courts.map((court) => mapCourtResponse(court));
}
export async function createCourt(appUserId: string, complexId: string, input: CreateCourtInput) {
await ensureComplexAccess(complexId, appUserId);
export async function createCourt(userId: string, complexId: string, input: CreateCourtInput) {
await ensureComplexAccess(complexId, userId);
await ensureActiveSport(input.sportId);
await enforcePlanCourtLimit(complexId);
assertAvailabilityRanges(input.availability);
@@ -249,7 +249,7 @@ export async function createCourt(appUserId: string, complexId: string, input: C
return court;
});
const court = await getCourtByIdForUser(createdCourt.id, appUserId);
const court = await getCourtByIdForUser(createdCourt.id, userId);
if (!court) {
throw new CourtServiceError('No se pudo obtener la cancha creada.', 404);
@@ -258,8 +258,8 @@ export async function createCourt(appUserId: string, complexId: string, input: C
return mapCourtResponse(court);
}
export async function updateCourt(appUserId: string, courtId: string, input: UpdateCourtInput) {
const existingCourt = await getCourtByIdForUser(courtId, appUserId);
export async function updateCourt(userId: string, courtId: string, input: UpdateCourtInput) {
const existingCourt = await getCourtByIdForUser(courtId, userId);
if (!existingCourt) {
throw new CourtServiceError('Cancha no encontrada.', 404);
@@ -305,7 +305,7 @@ export async function updateCourt(appUserId: string, courtId: string, input: Upd
}
});
const updatedCourt = await getCourtByIdForUser(courtId, appUserId);
const updatedCourt = await getCourtByIdForUser(courtId, userId);
if (!updatedCourt) {
throw new CourtServiceError('Cancha no encontrada.', 404);

View File

@@ -1,4 +1,5 @@
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';
@@ -10,7 +11,6 @@ import type {
} from '@repo/api-contract';
import type { User as SupabaseAuthUser } from '@supabase/supabase-js';
import { v7 as uuidv7 } from 'uuid';
import { auth } from '@/lib/auth';
const OTP_LENGTH = 6;
const OTP_TTL_MINUTES = Number(Bun.env.ONBOARDING_OTP_TTL_MINUTES ?? 10);
@@ -441,7 +441,6 @@ export async function completeOnboarding(input: OnboardingCompleteInput) {
const complexSlug = await buildUniqueComplexSlug(input.complexName);
const result = await db.$transaction(async (tx) => {
const complex = await tx.complex.create({
data: {
id: uuidv7(),

View File

@@ -2,7 +2,7 @@ import { getUserProfile } from '@/modules/user/services/user.service';
import type { AppContext } from '@/types/hono';
export function getUserProfileHandler(c: AppContext) {
const authUser = c.get('authUser');
const profile = getUserProfile(authUser);
const user = c.get('user');
const profile = getUserProfile(user);
return c.json(profile);
}

View File

@@ -1,23 +1,13 @@
import type { AuthUser } from '@/types/auth-user';
import type { User } from '@/lib/auth';
import type { UserProfile } from '@repo/api-contract';
export function getUserProfile(user: AuthUser): UserProfile {
const fullName =
(typeof user.user_metadata?.full_name === 'string' && user.user_metadata.full_name) ||
(typeof user.user_metadata?.name === 'string' && user.user_metadata.name) ||
(user.email ?? 'Usuario');
const role = (typeof user.app_metadata?.role === 'string' && user.app_metadata.role) || 'member';
const avatarUrl =
(typeof user.user_metadata?.avatar_url === 'string' && user.user_metadata.avatar_url) || null;
export function getUserProfile(user: User): UserProfile {
return {
id: user.id,
fullName,
email: user.email ?? null,
role,
avatarUrl,
createdAt: user.created_at,
fullName: user.name,
email: user.email,
role: (user as any).role || 'member',
avatarUrl: user.image || null,
createdAt: user.createdAt,
};
}

View File

@@ -1,5 +1,6 @@
import path from 'node:path';
import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
import { authRoutes } from '@/modules/auth/auth.routes';
import { complexRoutes } from '@/modules/complex/complex.routes';
import { courtRoutes } from '@/modules/court/court.routes';
import { onboardingRoutes } from '@/modules/onboarding/onboarding.routes';
@@ -8,14 +9,12 @@ import { planRoutes } from '@/modules/plan/plan.routes';
import { publicBookingRoutes } from '@/modules/public-booking/public-booking.routes';
import { sportRoutes } from '@/modules/sport/sport.routes';
import { userRoutes } from '@/modules/user/user.routes';
import { authRoutes } from '@/modules/auth/auth.routes';
import type { AppEnv } from '@/types/hono';
import type { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
import { healthCheckRoutes } from './modules/health-check/health-check.routes';
export function registerRoutes(app: Hono<AppEnv>) {
app
.route('', authRoutes)
.route('/api/user', userRoutes)

View File

@@ -1,9 +1,9 @@
import type { AuthUser } from '@/types/auth-user';
import type { Session, User } from '@/lib/auth';
import type { Context } from 'hono';
export type AppVariables = {
authUser: AuthUser;
appUserId: string;
user: User;
session: Session;
};
export type AppEnv = {

View File

@@ -29,7 +29,7 @@ import type {
UserProfileResponse,
} from '@repo/api-contract';
import axios, { AxiosError } from 'axios';
import { createAuthClient } from "better-auth/react";
import { createAuthClient } from 'better-auth/react';
const apiBaseUrl =
import.meta.env.VITE_API_BASE_URL?.trim() ||
@@ -37,9 +37,11 @@ const apiBaseUrl =
export const authClient = createAuthClient({
baseURL: apiBaseUrl,
fetchOptions: {
credentials: 'include',
},
});
export class ApiClientError extends Error {
status?: number;
details?: unknown;
@@ -66,6 +68,7 @@ const handlers: ErrorHandlers = {};
const http = axios.create({
baseURL: apiBaseUrl,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
@@ -285,6 +288,6 @@ export function configureApiClient(nextHandlers: ErrorHandlers) {
handlers.onError = nextHandlers.onError;
}
export function setApiAccessToken(token: string | null) {
export function setApiAccessToken(_token: string | null) {
// accessToken = token;
}

View File

@@ -1,14 +1,8 @@
import { setApiAccessToken } from '@/lib/api-client';
import { supabase } from '@/lib/supabase';
import type { Session, User } from '@supabase/supabase-js';
import {
type PropsWithChildren,
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { authClient } from '@/lib/api-client';
import { type PropsWithChildren, createContext, useContext, useMemo } from 'react';
export type User = typeof authClient.$Infer.Session.user;
export type Session = typeof authClient.$Infer.Session.session;
type SignInParams = {
email: string;
@@ -48,14 +42,7 @@ const AuthContext = createContext<AuthContextValue | null>(null);
function getDisplayName(user: User | null): string {
if (!user) return 'Usuario';
const fromMetadata =
user.user_metadata?.name ?? user.user_metadata?.full_name ?? user.user_metadata?.user_name;
if (typeof fromMetadata === 'string' && fromMetadata.trim().length > 0) {
return fromMetadata.trim();
}
if (user.name?.trim()) return user.name.trim();
return user.email ?? 'Usuario';
}
@@ -66,47 +53,14 @@ function getInitials(name: string): string {
}
function getAvatarUrl(user: User | null): string | null {
if (!user) return null;
const value = user.user_metadata?.avatar_url ?? user.user_metadata?.picture;
return typeof value === 'string' && value.length > 0 ? value : null;
return user?.image || null;
}
export function AuthProvider({ children }: PropsWithChildren) {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const { data, isPending } = authClient.useSession();
useEffect(() => {
let mounted = true;
const init = async () => {
const { data } = await supabase.auth.getSession();
if (!mounted) return;
setSession(data.session);
setUser(data.session?.user ?? null);
setApiAccessToken(data.session?.access_token ?? null);
setLoading(false);
};
void init();
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, nextSession) => {
setSession(nextSession);
setUser(nextSession?.user ?? null);
setApiAccessToken(nextSession?.access_token ?? null);
setLoading(false);
});
return () => {
mounted = false;
subscription.unsubscribe();
};
}, []);
const user = data?.user ?? null;
const session = data?.session ?? null;
const value = useMemo<AuthContextValue>(() => {
const displayName = getDisplayName(user);
@@ -116,13 +70,13 @@ export function AuthProvider({ children }: PropsWithChildren) {
return {
user,
session,
loading,
loading: isPending,
isAuthenticated: Boolean(user),
displayName,
initials,
avatarUrl,
signInWithPassword: async ({ email, password }) => {
const { error } = await supabase.auth.signInWithPassword({
const { error } = await authClient.signIn.email({
email,
password,
});
@@ -132,15 +86,10 @@ export function AuthProvider({ children }: PropsWithChildren) {
}
},
signUp: async ({ email, password, fullName }) => {
const { data, error } = await supabase.auth.signUp({
const { data: signUpData, error } = await authClient.signUp.email({
email,
password,
options: {
data: {
full_name: fullName,
name: fullName,
},
},
});
if (error) {
@@ -148,15 +97,12 @@ export function AuthProvider({ children }: PropsWithChildren) {
}
return {
requiresEmailConfirmation: !data.session,
requiresEmailConfirmation: !signUpData?.session,
};
},
updateProfile: async ({ fullName }) => {
const { error } = await supabase.auth.updateUser({
data: {
full_name: fullName,
const { error } = await authClient.user.update({
name: fullName,
},
});
if (error) {
@@ -164,8 +110,10 @@ export function AuthProvider({ children }: PropsWithChildren) {
}
},
updatePassword: async ({ password }) => {
const { error } = await supabase.auth.updateUser({
password,
// Better Auth uses changePassword for authenticated users
const { error } = await authClient.changePassword({
newPassword: password,
revokeOtherSessions: true,
});
if (error) {
@@ -173,13 +121,13 @@ export function AuthProvider({ children }: PropsWithChildren) {
}
},
signOut: async () => {
const { error } = await supabase.auth.signOut();
const { error } = await authClient.signOut();
if (error) {
throw error;
}
},
};
}, [loading, session, user]);
}, [isPending, session, user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

View File

@@ -1,18 +0,0 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
'Missing Supabase environment variables. Define VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.'
);
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
},
});