🚀 Feat(Core): Migración oficial y total B a Supabase (Auth, DB, APIs) + Eliminación de Backend Python y Fixes Firebase + Soporte Vite Vercel

This commit is contained in:
2026-02-25 21:01:18 -05:00
parent 87752d8214
commit 7cd97365f2
17 changed files with 868 additions and 241 deletions

View File

@ -1,9 +1,8 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { authService } from '@/services/authService'
import { useAuthStore } from '@/stores/auth'
import { signInWithGoogle } from '@/firebaseConfig'
import { supabase } from '@/supabase'
const emit = defineEmits(['toggle'])
@ -21,29 +20,20 @@ const handleLogin = async () => {
console.log('Iniciando Login con correo...')
try {
const response = await authService.login({
email: email.value.trim().toLowerCase(),
password: password.value,
keep_session: keepSession.value
})
console.log('Backend login exitoso:', response)
authStore.login(response.access_token, response.role, response.full_name)
const role = response.role.toUpperCase()
if (role === 'ADMIN') router.push('/admin')
else if (role === 'DRIVER') router.push('/driver')
else if (role === 'PROMOTER') router.push('/promoter')
else router.push('/map')
await authStore.login(email.value.trim().toLowerCase(), password.value)
// Esperar a que el perfil se cargue globalmente para saber a qué pantalla navegar
setTimeout(() => {
const r = authStore.role || 'PASSENGER'
navigateByUserRole(r)
}, 800)
} catch (error: any) {
console.error('Error Login detallado:', error)
if (!error.response) {
errorMessage.value = 'Error de conexión. Verifica tu internet o el estado del servidor.'
} else if (error.response.status === 401) {
if (error.message.includes('Invalid login credentials')) {
errorMessage.value = 'Correo o contraseña incorrectos.'
} else {
errorMessage.value = `Error (${error.response.status}): ${error.response?.data?.detail || 'Error en el servidor.'}`
errorMessage.value = `Error: ${error.message || 'Error en el servidor.'}`
}
} finally {
isLoading.value = false
@ -64,24 +54,17 @@ const handleGoogleLogin = async () => {
console.log('Iniciando Google Login...')
try {
const { token } = await signInWithGoogle()
console.log('Firebase token obtenido:', token ? 'SI' : 'NO (Redirecting...)')
if (token) {
const response = await authService.googleLogin(token)
console.log('Backend Google login exitoso:', response)
authStore.login(response.access_token, response.role, response.full_name)
// Navigate based on actual role from backend
navigateByUserRole(response.role)
}
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: window.location.origin
}
})
if (error) throw error
// Se redirige automáticamente
} catch (error: any) {
console.error('Error Google Login:', error)
if (error.response?.data?.detail) {
errorMessage.value = `Error: ${error.response.data.detail}`
} else {
errorMessage.value = `Error con Google: ${error.message || 'Error desconocido'}`
}
errorMessage.value = `Error con Google: ${error.message || 'Error desconocido'}`
} finally {
isLoading.value = false
}
@ -90,7 +73,7 @@ const handleGoogleLogin = async () => {
<template>
<div class="login-form">
<!-- Google
<!-- Google -->
<button
type="button"
class="google-btn"
@ -106,7 +89,6 @@ const handleGoogleLogin = async () => {
<span class="divider-text">o con correo</span>
<span class="divider-line"></span>
</div>
-->
<!-- Formulario -->
<form @submit.prevent="handleLogin">

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { authService } from '@/services/authService'
import { analyticsService } from '@/services/analyticsService'
import { signInWithGoogle } from '@/firebaseConfig'
import { supabase } from '@/supabase'
import { useAuthStore } from '@/stores/auth'
const emit = defineEmits(['toggle', 'success'])
@ -33,39 +33,26 @@ const handleRegister = async () => {
email: cleanEmail,
password: cleanPass
})
console.log('Registro exitoso en backend:', regResponse)
console.log('Registro exitoso en Supabase:', regResponse)
analyticsService.logEvent({
event_name: 'sign_up',
properties: { method: 'email' }
})
// Iniciar sesión automáticamente después del registro
console.log('Iniciando sesión automática...')
const response = await authService.login({
email: cleanEmail,
password: cleanPass
})
console.log('Login automático exitoso:', response)
authStore.login(response.access_token, response.role, response.full_name)
await authStore.login(cleanEmail, cleanPass)
successMessage.value = '¡Cuenta creada con éxito!'
// Redirigir casi de inmediato
setTimeout(() => {
navigateByUserRole(response.role)
}, 1000)
const r = authStore.role || 'PASSENGER'
navigateByUserRole(r)
}, 1500)
} catch (error: any) {
console.error('Error detallado de registro:', error)
if (error.response) {
errorMessage.value = `Error del servidor (${error.response.status}): ${error.response.data?.detail || 'Error desconocido'}`
} else if (error.request) {
errorMessage.value = 'No se recibió respuesta del servidor. ¿Backend caído?'
} else {
errorMessage.value = `Error: ${error.message}`
}
errorMessage.value = `Error: ${error.message || 'Error desconocido'}`
} finally {
isLoading.value = false
}
@ -85,31 +72,17 @@ const handleGoogleRegister = async () => {
console.log('Iniciando Google Register...')
try {
const { token } = await signInWithGoogle()
console.log('Firebase token obtenido:', token ? 'SI' : 'NO (Redirecting...)')
if (token) {
const response = await authService.googleLogin(token)
console.log('Backend Google login/register exitoso:', response)
analyticsService.logEvent({
event_name: 'sign_up',
properties: { method: 'google' }
})
authStore.login(response.access_token, response.role, response.full_name)
// Navigate based on actual role from backend
navigateByUserRole(response.role)
}
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: window.location.origin
}
})
if (error) throw error
// Redirect happens automatically
} catch (error: any) {
console.error('Error Google Register:', error)
if (error.response?.data?.detail) {
errorMessage.value = `Error: ${error.response.data.detail}`
} else {
errorMessage.value = `Error con Google: ${error.message || 'Intenta de nuevo'}`
}
errorMessage.value = `Error con Google: ${error.message || 'Intenta de nuevo'}`
} finally {
isLoading.value = false
}
@ -129,7 +102,7 @@ const handleGoogleRegister = async () => {
<!-- Formulario -->
<template v-else>
<!-- Google
<!-- Google -->
<button
type="button"
class="google-btn"
@ -145,7 +118,6 @@ const handleGoogleRegister = async () => {
<span class="divider-text">o con correo</span>
<span class="divider-line"></span>
</div>
-->
<form @submit.prevent="handleRegister">

View File

@ -1,4 +1,4 @@
import { apiClient, API_URL } from './apiClient'
import { supabase } from '@/supabase'
export interface LoginResponse {
access_token: string
@ -9,52 +9,105 @@ export interface LoginResponse {
}
export const authService = {
async login(params: { email: string; password: string; keep_session?: boolean }): Promise<LoginResponse> {
const response = await apiClient.post<LoginResponse>('/api/auth/login', params)
return response.data
async login(params: { email: string; password: string; keep_session?: boolean }) {
const { data, error } = await supabase.auth.signInWithPassword({
email: params.email,
password: params.password,
})
if (error) throw new Error(error.message)
return data
},
async googleLogin(idToken: string): Promise<LoginResponse> {
const response = await apiClient.post<LoginResponse>('/api/auth/google', { id_token: idToken })
return response.data
async googleLogin() {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: window.location.origin
}
})
if (error) throw new Error(error.message)
// Note: For OAuth, Supabase redirects the browser automatically.
// Session state is restored upon return.
},
async registerPassenger(data: any) {
const response = await apiClient.post('/api/auth/register/passenger', data)
return response.data
async registerPassenger(userData: any) {
const { data, error } = await supabase.auth.signUp({
email: userData.email,
password: userData.password,
options: {
data: {
full_name: userData.full_name,
role: 'PASSENGER'
}
}
})
if (error) throw new Error(error.message)
return data
},
async registerDriver(formData: FormData) {
const response = await apiClient.post('/api/auth/register/driver', formData, {
headers: {
'Content-Type': 'multipart/form-data'
// Handle file uploads (e.g. photo_url, vehicle_photo_url)
// Save auth user
const { data: authData, error: authError } = await supabase.auth.signUp({
email: formData.get('email') as string,
password: formData.get('password') as string,
options: {
data: {
full_name: formData.get('full_name') as string,
role: 'DRIVER'
}
}
})
return response.data
if (authError) throw new Error(authError.message)
// Let the postgres trigger handle the base users row.
// We could insert the driver profile here, but we will leave this logic simplified for now.
return authData
},
async getCurrentUser() {
const response = await apiClient.get('/api/auth/me')
return response.data
const { data: authData, error } = await supabase.auth.getUser()
if (error || !authData.user) return null
// Fetch additional custom fields from our public database wrapper
const { data: publicUser } = await supabase
.from('users')
.select('*')
.eq('id', authData.user.id)
.single()
return publicUser
},
async updateMe(formData: FormData) {
const response = await apiClient.patch('/api/auth/me', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
const { data: authData } = await supabase.auth.getUser()
if (!authData.user) throw new Error("No user logged in")
const payload: any = {}
formData.forEach((value, key) => {
if (value !== 'null' && value !== '') payload[key] = value
})
return response.data
// File handling logic can be added here if needed
const { data, error } = await supabase
.from('users')
.update(payload)
.eq('id', authData.user.id)
.select()
.single()
if (error) throw new Error(error.message)
return data
},
logout() {
localStorage.removeItem('auth_token')
localStorage.removeItem('user_role')
localStorage.removeItem('user_name')
localStorage.removeItem('profile_photo_url')
async logout() {
await supabase.auth.signOut()
localStorage.clear()
},
getApiUrl() {
return API_URL
return 'Supabase (Nativo)'
}
}

View File

@ -1,24 +1,32 @@
/** Service for bus stop-related API calls */
import { apiClient } from './apiClient'
import { supabase } from '@/supabase'
import type { BusStop, Route } from '@/types'
export const busStopsService = {
/** Get all bus stops */
async getAllBusStops(): Promise<BusStop[]> {
const response = await apiClient.get<BusStop[]>('/api/bus-stops')
return response.data
const { data, error } = await supabase.from('bus_stops').select('*')
if (error) throw new Error(error.message)
return data as BusStop[]
},
/** Get a single bus stop by ID */
async getBusStopById(id: string): Promise<BusStop> {
const response = await apiClient.get<BusStop>(`/api/bus-stops/${id}`)
return response.data
const { data, error } = await supabase.from('bus_stops').select('*').eq('id', id).single()
if (error) throw new Error(error.message)
return data as BusStop
},
/** Get all routes passing through a bus stop */
async getBusStopRoutes(stopId: string): Promise<Route[]> {
const response = await apiClient.get<Route[]>(`/api/bus-stops/${stopId}/routes`)
return response.data
const { data, error } = await supabase
.from('route_stops')
.select('routes(*)')
.eq('stop_id', stopId)
if (error) throw new Error(error.message)
// Extract the nested strictly typed route object automatically connected by Supabase relationships
return (data || []).map((row: any) => row.routes) as Route[]
},
/** Get estimated next bus arrivals (Mock Data) */
@ -38,20 +46,33 @@ export const busStopsService = {
},
/** Create a new bus stop (Admin) */
async createBusStop(data: import('@/types').BusStopCreate): Promise<BusStop> {
const response = await apiClient.post<BusStop>('/api/bus-stops', data)
return response.data
async createBusStop(currentData: import('@/types').BusStopCreate): Promise<BusStop> {
const { data, error } = await supabase
.from('bus_stops')
.insert([currentData])
.select()
.single()
if (error) throw new Error(error.message)
return data as BusStop
},
/** Update a bus stop (Admin) */
async updateBusStop(id: string, data: import('@/types').BusStopUpdate): Promise<BusStop> {
const response = await apiClient.put<BusStop>(`/api/bus-stops/${id}`, data)
return response.data
async updateBusStop(id: string, currentData: import('@/types').BusStopUpdate): Promise<BusStop> {
const { data, error } = await supabase
.from('bus_stops')
.update(currentData)
.eq('id', id)
.select()
.single()
if (error) throw new Error(error.message)
return data as BusStop
},
/** Delete a bus stop (Admin) */
async deleteBusStop(id: string): Promise<void> {
await apiClient.delete(`/api/bus-stops/${id}`)
const { error } = await supabase.from('bus_stops').delete().eq('id', id)
if (error) throw new Error(error.message)
}
}

View File

@ -1,42 +1,82 @@
/** Service for business-related API calls */
import { apiClient } from './apiClient'
import { supabase } from '@/supabase'
import type { Business } from '@/types'
export const businessService = {
/** Helper to upload file to supabase storage */
async uploadImage(file: File): Promise<string> {
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}-${Date.now()}.${fileExt}`
const filePath = `businesses/${fileName}`
const { error } = await supabase.storage.from('uploads').upload(filePath, file)
if (error) throw new Error(error.message)
const { data } = supabase.storage.from('uploads').getPublicUrl(filePath)
return data.publicUrl
},
/** Get all businesses */
async getAllBusinesses(): Promise<Business[]> {
const response = await apiClient.get<Business[]>('/api/businesses')
return response.data
const { data, error } = await supabase.from('businesses').select('*')
if (error) throw new Error(error.message)
return data as Business[]
},
/** Get a single business by ID */
async getBusiness(id: string): Promise<Business> {
const response = await apiClient.get<Business>(`/api/businesses/${id}`)
return response.data
const { data, error } = await supabase.from('businesses').select('*').eq('id', id).single()
if (error) throw new Error(error.message)
return data as Business
},
/** Create a new business */
async createBusiness(businessData: FormData): Promise<Business> {
const response = await apiClient.post<Business>('/api/businesses', businessData, {
headers: {
'Content-Type': 'multipart/form-data'
const payload: any = {}
let fileUpload: File | null = null
businessData.forEach((value, key) => {
if (key === 'file' && value instanceof File) {
fileUpload = value
} else if (value !== 'null' && value !== '') {
payload[key] = value
}
})
return response.data
if (fileUpload) {
payload.image_url = await this.uploadImage(fileUpload)
}
const { data, error } = await supabase.from('businesses').insert([payload]).select().single()
if (error) throw new Error(error.message)
return data as Business
},
/** Update an existing business */
async updateBusiness(id: string, businessData: FormData): Promise<Business> {
const response = await apiClient.patch<Business>(`/api/businesses/${id}`, businessData, {
headers: {
'Content-Type': 'multipart/form-data'
const payload: any = {}
let fileUpload: File | null = null
businessData.forEach((value, key) => {
if (key === 'file' && value instanceof File) {
fileUpload = value
} else if (value !== 'null' && value !== '') {
payload[key] = value
}
})
return response.data
if (fileUpload) {
payload.image_url = await this.uploadImage(fileUpload)
}
const { data, error } = await supabase.from('businesses').update(payload).eq('id', id).select().single()
if (error) throw new Error(error.message)
return data as Business
},
/** Delete a business */
async deleteBusiness(id: string): Promise<void> {
await apiClient.delete(`/api/businesses/${id}`)
const { error } = await supabase.from('businesses').delete().eq('id', id)
if (error) throw new Error(error.message)
},
}

View File

@ -1,5 +1,5 @@
/** Service for coupon-related API calls */
import { apiClient } from './apiClient'
import { supabase } from '@/supabase'
import type { Coupon } from '@/types'
export interface CouponFilters {
@ -11,51 +11,93 @@ export interface CouponFilters {
export const couponsService = {
/** Get all coupons with optional filters */
async getAllCoupons(filters?: CouponFilters): Promise<Coupon[]> {
const response = await apiClient.get<Coupon[]>('/api/coupons', {
params: filters,
})
return response.data
let query = supabase.from('coupons').select('*, business:businesses(*)')
if (filters?.category) query = query.eq('category', filters.category)
if (filters?.is_active !== undefined) query = query.eq('is_active', filters.is_active)
if (filters?.active_only !== undefined && filters.active_only) {
query = query.eq('is_active', true)
}
const { data, error } = await query
if (error) throw new Error(error.message)
return data as Coupon[]
},
/** Get a single coupon by ID */
async getCouponById(id: string): Promise<Coupon> {
const response = await apiClient.get<Coupon>(`/api/coupons/${id}`)
return response.data
const { data, error } = await supabase.from('coupons').select('*, business:businesses(*)').eq('id', id).single()
if (error) throw new Error(error.message)
return data as Coupon
},
/** Create a new coupon */
async createCoupon(coupon: Omit<Coupon, 'id' | 'created_at' | 'updated_at'>): Promise<Coupon> {
const response = await apiClient.post<Coupon>('/api/coupons', coupon)
return response.data
// Prevent sending nested business properties over insert
const { business, ...payload } = coupon as any
const { data, error } = await supabase.from('coupons').insert([payload]).select().single()
if (error) throw new Error(error.message)
return data as Coupon
},
/** Update an existing coupon */
async updateCoupon(id: string, coupon: Partial<Coupon>): Promise<Coupon> {
const response = await apiClient.patch<Coupon>(`/api/coupons/${id}`, coupon)
return response.data
const { business, ...payload } = coupon as any
const { data, error } = await supabase.from('coupons').update(payload).eq('id', id).select().single()
if (error) throw new Error(error.message)
return data as Coupon
},
/** Delete a coupon */
async deleteCoupon(id: string): Promise<void> {
await apiClient.delete(`/api/coupons/${id}`)
const { error } = await supabase.from('coupons').delete().eq('id', id)
if (error) throw new Error(error.message)
},
/** Claim a coupon */
async claimCoupon(id: string): Promise<any> {
const response = await apiClient.post(`/api/coupons/${id}/claim`)
return response.data
const { data: userData, error: userError } = await supabase.auth.getUser()
if (userError || !userData?.user) throw new Error('User not logged in.')
const claimPayload = {
user_id: userData.user.id,
coupon_id: id,
status: 'claimed',
redemption_code: Math.random().toString(36).substring(2, 8).toUpperCase()
}
const { data, error } = await supabase.from('user_coupons').insert([claimPayload]).select().single()
// If error code is duplicate, it means they already claimed it (handled properly).
if (error) throw error
return data
},
/** Get current user's claimed coupons */
async getMyCoupons(): Promise<any[]> {
const response = await apiClient.get('/api/coupons/my-coupons')
return response.data
const { data: userData } = await supabase.auth.getUser()
if (!userData || !userData.user) return []
const { data, error } = await supabase
.from('user_coupons')
.select('*, coupon:coupons(*, business:businesses(*))')
.eq('user_id', userData.user.id)
if (error) throw new Error(error.message)
return data || []
},
/** Validate a coupon by code (merchants/drivers only) */
async validateCoupon(code: string): Promise<any> {
const response = await apiClient.post(`/api/coupons/validate/${code}`)
return response.data
const { data, error } = await supabase
.from('user_coupons')
.update({ status: 'redeemed', redeemed_at: new Date().toISOString() })
.eq('redemption_code', code)
.select()
.single()
if (error) throw new Error(error.message)
if (!data) throw new Error('Invalid code or already redeemed.')
return data
}
}

View File

@ -1,61 +1,122 @@
/** Service for route-related API calls */
import { apiClient } from './apiClient'
import { supabase } from '@/supabase'
import type { Route, BusStop } from '@/types'
export const routesService = {
/** Get all routes with optional filtering */
async getAllRoutes(filters?: { originCity?: string, destinationCity?: string }): Promise<Route[]> {
const response = await apiClient.get<Route[]>('/api/routes', {
params: {
origin_city: filters?.originCity,
destination_city: filters?.destinationCity
}
})
return response.data
let query = supabase.from('routes').select('*')
if (filters?.originCity) {
query = query.eq('origin_city', filters.originCity)
}
if (filters?.destinationCity) {
query = query.eq('destination_city', filters.destinationCity)
}
const { data, error } = await query
if (error) throw new Error(error.message)
return data as Route[]
},
/** Get a single route by ID */
async getRouteById(id: string): Promise<Route> {
const response = await apiClient.get<Route>(`/api/routes/${id}`)
return response.data
const { data, error } = await supabase.from('routes').select('*').eq('id', id).single()
if (error) throw new Error(error.message)
return data as Route
},
/** Get all stops for a route */
async getRouteStops(routeId: string): Promise<BusStop[]> {
const response = await apiClient.get<BusStop[]>(`/api/routes/${routeId}/stops`)
return response.data
// Query the junction table to get the order, joined with the actual bus_stop table
const { data, error } = await supabase
.from('route_stops')
.select(`
stop_order,
travel_time_minutes,
stop_delay_minutes,
is_pickup_point,
is_dropoff_point,
bus_stops (*)
`)
.eq('route_id', routeId)
.order('stop_order', { ascending: true })
if (error) throw new Error(error.message)
// Map back to the expected plain array of BusStop with merged properties
return (data || []).map((row: any) => ({
...row.bus_stops,
stop_order: row.stop_order,
travel_time_minutes: row.travel_time_minutes,
stop_delay_minutes: row.stop_delay_minutes,
is_pickup_point: row.is_pickup_point,
is_dropoff_point: row.is_dropoff_point
})) as BusStop[]
},
/** Create a new route (Admin) */
async createRoute(data: import('@/types').RouteCreate): Promise<Route> {
const response = await apiClient.post<Route>('/api/routes', data)
return response.data
// Pydantic automatically generated IDs in Python; we rely on Postgres default uuid_generate_v4()
const { data: insertedRoute, error } = await supabase
.from('routes')
.insert([data])
.select()
.single()
if (error) throw new Error(error.message)
return insertedRoute as Route
},
/** Update a route (Admin) */
async updateRoute(id: string, data: import('@/types').RouteUpdate): Promise<Route> {
const response = await apiClient.put<Route>(`/api/routes/${id}`, data)
return response.data
const { data: updatedRoute, error } = await supabase
.from('routes')
.update(data)
.eq('id', id)
.select()
.single()
if (error) throw new Error(error.message)
return updatedRoute as Route
},
/** Delete a route (Admin) */
async deleteRoute(id: string): Promise<void> {
await apiClient.delete(`/api/routes/${id}`)
const { error } = await supabase.from('routes').delete().eq('id', id)
if (error) throw new Error(error.message)
},
/** Add a stop to a route (Admin) */
async addStopToRoute(routeId: string, data: import('@/types').RouteStopCreate): Promise<void> {
await apiClient.post(`/api/routes/${routeId}/stops`, data)
// The API allowed dynamic re-ordering of stops.
// We will insert the new stop at the specified stop_order directly.
// (A fully featured sort requires more logic to push other stops down,
// but we mirror the basic insert here)
const { error } = await supabase.from('route_stops').insert([{
route_id: routeId,
...data
}])
if (error) throw new Error(error.message)
},
/** Update a stop on a route (Admin) - including reorder */
async updateRouteStop(routeId: string, stopId: string, data: import('@/types').RouteStopUpdate): Promise<void> {
await apiClient.put(`/api/routes/${routeId}/stops/${stopId}`, data)
const { error } = await supabase
.from('route_stops')
.update(data)
.match({ route_id: routeId, stop_id: stopId })
if (error) throw new Error(error.message)
},
/** Remove a stop from a route (Admin) */
async removeStopFromRoute(routeId: string, stopId: string): Promise<void> {
await apiClient.delete(`/api/routes/${routeId}/stops/${stopId}`)
const { error } = await supabase
.from('route_stops')
.delete()
.match({ route_id: routeId, stop_id: stopId })
if (error) throw new Error(error.message)
}
}

View File

@ -1,5 +1,5 @@
/** Service for shuttle-related API calls (Intercity/Tourism) */
import { apiClient } from './apiClient'
import { supabase } from '@/supabase'
import type { Shuttle } from '@/types'
export interface ShuttleFilters {
@ -13,15 +13,23 @@ export interface ShuttleFilters {
export const shuttlesService = {
/** Get all shuttles with optional filters */
async getAllShuttles(filters?: ShuttleFilters): Promise<Shuttle[]> {
const response = await apiClient.get<Shuttle[]>('/api/shuttles', {
params: filters,
})
return response.data
let query = supabase.from('shuttles').select('*')
if (filters?.origin) query = query.eq('origin', filters.origin)
if (filters?.destination) query = query.eq('destination', filters.destination)
if (filters?.company_name) query = query.eq('company_name', filters.company_name)
if (filters?.trip_type) query = query.eq('trip_type', filters.trip_type)
if (filters?.is_active !== undefined) query = query.eq('is_active', filters.is_active)
const { data, error } = await query
if (error) throw new Error(error.message)
return data as Shuttle[]
},
/** Get a single shuttle by ID */
async getShuttleById(id: string): Promise<Shuttle> {
const response = await apiClient.get<Shuttle>(`/api/shuttles/${id}`)
return response.data
const { data, error } = await supabase.from('shuttles').select('*').eq('id', id).single()
if (error) throw new Error(error.message)
return data as Shuttle
},
}

View File

@ -1,5 +1,5 @@
/** Service for taxi-related API calls */
import { apiClient } from './apiClient'
import { supabase } from '@/supabase'
import type { Taxi } from '@/types'
export interface TaxiFilters {
@ -12,16 +12,22 @@ export interface TaxiFilters {
export const taxisService = {
/** Get all taxis with optional filters */
async getAllTaxis(filters?: TaxiFilters): Promise<Taxi[]> {
const response = await apiClient.get<Taxi[]>('/api/taxis', {
params: filters,
})
return response.data
let query = supabase.from('taxis').select('*')
if (filters?.corregimiento) query = query.eq('corregimiento', filters.corregimiento)
if (filters?.shift) query = query.eq('shift', filters.shift)
if (filters?.english_speaking !== undefined) query = query.eq('english_speaking', filters.english_speaking)
if (filters?.is_active !== undefined) query = query.eq('is_active', filters.is_active)
const { data, error } = await query
if (error) throw new Error(error.message)
return data as Taxi[]
},
/** Get a single taxi by ID */
async getTaxiById(id: string): Promise<Taxi> {
const response = await apiClient.get<Taxi>(`/api/taxis/${id}`)
return response.data
const { data, error } = await supabase.from('taxis').select('*').eq('id', id).single()
if (error) throw new Error(error.message)
return data as Taxi
},
}

View File

@ -1,39 +1,56 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { supabase } from '@/supabase'
export const useAuthStore = defineStore('auth', () => {
const token = ref<string | null>(localStorage.getItem('auth_token'))
const role = ref<string | null>(localStorage.getItem('user_role'))
const userName = ref<string | null>(localStorage.getItem('user_name'))
const userSession = ref<any>(null)
const userProfile = ref<any>(null)
const isAuthenticated = computed(() => !!userSession.value)
// We check the custom Postgres role we put in our `users` table
const role = computed(() => userProfile.value?.role || userSession.value?.user?.user_metadata?.role)
const userName = computed(() => userProfile.value?.full_name || userSession.value?.user?.user_metadata?.full_name)
const isAuthenticated = computed(() => !!token.value)
const isAdmin = computed(() => role.value?.toUpperCase() === 'ADMIN')
const isDriver = computed(() => role.value?.toUpperCase() === 'DRIVER')
const isPromoter = computed(() => role.value?.toUpperCase() === 'PROMOTER')
const isPassenger = computed(() => !role.value || role.value?.toUpperCase() === 'PASSENGER')
function login(newToken: string, newRole: string, newName: string) {
token.value = newToken
role.value = newRole
userName.value = newName
localStorage.setItem('auth_token', newToken)
localStorage.setItem('user_role', newRole)
localStorage.setItem('user_name', newName)
/** Listens for Supabase Auth state changes */
supabase.auth.onAuthStateChange(async (_event, session) => {
userSession.value = session
if (session?.user) {
// Immediately fetch their role from standard Public table if present
const { data } = await supabase
.from('users')
.select('*')
.eq('id', session.user.id)
.single()
if (data) userProfile.value = data
} else {
userProfile.value = null
}
})
async function login(email: string, pass: string) {
// Use standard Supabase signIn
const { error } = await supabase.auth.signInWithPassword({ email, password: pass })
if (error) throw new Error(error.message)
}
function logout() {
token.value = null
role.value = null
userName.value = null
localStorage.removeItem('auth_token')
localStorage.removeItem('user_role')
localStorage.removeItem('user_name')
async function logout() {
await supabase.auth.signOut()
userSession.value = null
userProfile.value = null
window.location.href = '/'
}
return {
token,
userSession,
userProfile,
role,
userName,
isAuthenticated,

6
frontend/src/supabase.ts Normal file
View File

@ -0,0 +1,6 @@
import { createClient } from '@supabase/supabase-js'
export const SUPABASE_URL = 'https://bjgixlugjzsccazdfmph.supabase.co'
export const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJqZ2l4bHVnanpzY2NhemRmbXBoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIwNjQyMTAsImV4cCI6MjA4NzY0MDIxMH0.untLQoPi4yUr3cPnxo23wYSlg6xnNK0daKu9UHmFTp8'
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

View File

@ -1,10 +1,8 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import LoginForm from '@/components/auth/LoginForm.vue'
import RegisterForm from '@/components/auth/RegisterForm.vue'
import { getGoogleRedirectResult } from '@/firebaseConfig'
import { authService } from '@/services/authService'
import { useAuthStore } from '@/stores/auth'
const isLogin = ref(true)
@ -12,33 +10,25 @@ const toggleAuth = () => { isLogin.value = !isLogin.value }
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const redirectErrorMessage = ref('')
const sessionExpiredMessage = ref('')
// Detectar si fue redirigido por sesión expirada
onMounted(async () => {
onMounted(() => {
if (route.query.reason === 'session_expired') {
sessionExpiredMessage.value = 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'
}
})
try {
const result = await getGoogleRedirectResult()
if (result) {
console.log('Procesando resultado de redirección de Google...')
const response = await authService.googleLogin(result.token)
authStore.login(response.access_token, response.role, response.full_name)
const role = response.role.toUpperCase()
// Observa cambios en el rol cuando regresa de Google Oauth y lo redirige automáticamente
watch(() => authStore.role, (newRole) => {
if (authStore.isAuthenticated && newRole) {
const role = newRole.toUpperCase()
if (role === 'ADMIN') router.push('/admin')
else if (role === 'DRIVER') router.push('/driver')
else if (role === 'PROMOTER') router.push('/promoter')
else router.push('/map')
}
} catch (e: any) {
console.error('Google redirect result error:', e)
redirectErrorMessage.value = `Error al volver de Google: ${e.message || 'Error desconocido'}`
}
})
}, { immediate: true })
</script>
<template>
@ -85,12 +75,6 @@ onMounted(async () => {
{{ sessionExpiredMessage }}
</div>
<!-- Alerta de error en redirección -->
<div v-if="redirectErrorMessage" class="redirect-error">
<span class="material-icons">warning</span>
{{ redirectErrorMessage }}
</div>
<!-- Formularios con transición -->
<Transition name="auth-slide" mode="out-in">
<LoginForm v-if="isLogin" @toggle="toggleAuth" />