🚀 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:
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
|
||||
@ -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)'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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
6
frontend/src/supabase.ts
Normal 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)
|
||||
@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user