fix: manejar error 401 automaticamente y agregar migracion de columnas routes
This commit is contained in:
@ -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
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user