fix: manejar error 401 automaticamente y agregar migracion de columnas routes

This commit is contained in:
2026-02-25 19:49:29 -05:00
parent fd95df461b
commit dc827bcbf4
4 changed files with 83 additions and 4 deletions

View File

@ -0,0 +1,38 @@
"""Fix routes table: ensure color, direction, average_speed_kmh columns exist
This migration uses IF NOT EXISTS to safely add missing columns regardless
of the current database state in production.
Revision ID: a1b2c3d4e5f6
Revises: ffcd1234abcd
Create Date: 2026-02-26 00:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a1b2c3d4e5f6'
down_revision: Union[str, None] = 'ffcd1234abcd'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Use raw SQL with IF NOT EXISTS to be safe regardless of prior migration state.
# This ensures the columns exist in production even if the previous migration
# had issues being applied.
with op.get_context().autocommit_block():
op.execute("ALTER TABLE routes ADD COLUMN IF NOT EXISTS color VARCHAR DEFAULT '#FEE715'")
op.execute("ALTER TABLE routes ADD COLUMN IF NOT EXISTS direction VARCHAR DEFAULT 'outbound'")
op.execute("ALTER TABLE routes ADD COLUMN IF NOT EXISTS average_speed_kmh FLOAT")
# Update any existing NULLs
op.execute("UPDATE routes SET color = '#FEE715' WHERE color IS NULL")
op.execute("UPDATE routes SET direction = 'outbound' WHERE direction IS NULL")
def downgrade() -> None:
pass

View File

@ -31,6 +31,22 @@ client.interceptors.response.use(
(error) => { (error) => {
if (error.response) { if (error.response) {
console.error('API Error:', error.response.status, error.response.data) console.error('API Error:', error.response.status, error.response.data)
// Si el token expiró o es inválido, limpiar sesión y redirigir al login
if (error.response.status === 401) {
const currentPath = window.location.pathname
// Solo redirigir si no estamos ya en la página de login
if (!currentPath.includes('/auth') && !currentPath.includes('/login')) {
localStorage.removeItem('auth_token')
localStorage.removeItem('user_role')
localStorage.removeItem('user_name')
localStorage.removeItem('profile_photo_url')
// Redirigir al login con mensaje
window.location.href = '/auth?reason=session_expired'
}
}
} else if (error.request) {
// La solicitud fue hecha pero no hubo respuesta (timeout, servidor dormido, etc.)
console.error('Network Error: No response from server', error.message)
} }
return Promise.reject(error) return Promise.reject(error)
} }

View File

@ -354,12 +354,25 @@ async function confirmCreateRoute() {
isCreating.value = false isCreating.value = false
} catch (err: any) { } catch (err: any) {
console.error('Error creating route:', err) console.error('Error creating route:', err)
if (err.response?.status === 401) {
// El interceptor ya redirige al login, pero mostramos aviso
alert('Tu sesión ha expirado. Serás redirigido al inicio de sesión.')
return
} else if (err.response?.status === 403) {
alert('No tienes permisos de administrador para crear rutas.')
return
} else if (!err.response && err.request) {
// Network Error - servidor no respondió
alert('No se pudo conectar al servidor. Si es la primera solicitud del día, el servidor puede tardar ~30 segundos en iniciar. Por favor, intenta de nuevo en un momento.')
return
}
const errorMsg = err.response?.data?.detail const errorMsg = err.response?.data?.detail
|| err.response?.data?.message || err.response?.data?.message
|| err.message || err.message
|| 'Error desconocido' || 'Error desconocido'
const errorDetail = err.response ? `Status: ${err.response.status}` : 'No hubo respuesta del servidor (Network Error)' alert(`No se pudo crear la ruta: ${errorMsg}`)
alert(`No se pudo crear la ruta: ${errorMsg}\n\nDetalle: ${errorDetail}`)
} }
} }

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import LoginForm from '@/components/auth/LoginForm.vue' import LoginForm from '@/components/auth/LoginForm.vue'
import RegisterForm from '@/components/auth/RegisterForm.vue' import RegisterForm from '@/components/auth/RegisterForm.vue'
import { getGoogleRedirectResult } from '@/firebaseConfig' import { getGoogleRedirectResult } from '@/firebaseConfig'
@ -10,11 +10,17 @@ import { useAuthStore } from '@/stores/auth'
const isLogin = ref(true) const isLogin = ref(true)
const toggleAuth = () => { isLogin.value = !isLogin.value } const toggleAuth = () => { isLogin.value = !isLogin.value }
const router = useRouter() const router = useRouter()
const route = useRoute()
const authStore = useAuthStore() const authStore = useAuthStore()
const redirectErrorMessage = ref('') const redirectErrorMessage = ref('')
const sessionExpiredMessage = ref('')
// Handle redirect result from Google Sign-In on mobile // Detectar si fue redirigido por sesión expirada
onMounted(async () => { onMounted(async () => {
if (route.query.reason === 'session_expired') {
sessionExpiredMessage.value = 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'
}
try { try {
const result = await getGoogleRedirectResult() const result = await getGoogleRedirectResult()
if (result) { if (result) {
@ -73,6 +79,12 @@ onMounted(async () => {
</button> </button>
</div> </div>
<!-- Alerta de sesión expirada -->
<div v-if="sessionExpiredMessage" class="redirect-error" style="background: rgba(234, 179, 8, 0.1); border-color: rgba(234, 179, 8, 0.2); color: #eab308;">
<span class="material-icons">lock_clock</span>
{{ sessionExpiredMessage }}
</div>
<!-- Alerta de error en redirección --> <!-- Alerta de error en redirección -->
<div v-if="redirectErrorMessage" class="redirect-error"> <div v-if="redirectErrorMessage" class="redirect-error">
<span class="material-icons">warning</span> <span class="material-icons">warning</span>