Login de Usuarios por Vertical (Strapi)
En este apartado se documenta el flujo completo de inicio de sesión de usuarios a través del endpoint personalizado de Strapi /api/user-endpoints/login, desde el frontend Nuxt 3.
🧠 Objetivo
Section titled “🧠 Objetivo”Permitir el login de usuarios segmentado por vertical, enviando un payload adaptado y recibiendo el JWT personalizado desde el backend, para luego almacenarlo de forma segura en localStorage.
📁 Ubicación del servicio
Section titled “📁 Ubicación del servicio”/services/strapi/login.ts
🧱 Estructura del servicio
Section titled “🧱 Estructura del servicio”El servicio loginUserForVertical adapta el payload enviado por el formulario (por ejemplo, LoginForm.vue) al formato que espera el endpoint de Strapi.
import type { LoginPayload, LoginResponse } from '@/interfaces/api/strapi/login'import type { LanguageCode } from '@/types/common'import type { Strapi } from '@/interfaces/api/common'import { isProduction } from '@/core/utils'import { visibleVertical } from '@/core/config'
export const loginUserForVertical = async ( _lang: LanguageCode, params: LoginPayload,): Promise<Strapi<LoginResponse> | null> => { /** * Selecciona el vertical actual según el entorno (producción o desarrollo) * y se obtiene la URL base de Strapi eliminando una barra final si existe */ const strapiBaseUrl = ( isProduction() ? visibleVertical.productionApi : visibleVertical.developmentApi ).replace(/\/$/, '')
try { // Hace una petición POST al endpoint de login de Strapi const { data, error, status } = await useFetch<LoginResponse>( `${strapiBaseUrl}/api/user-endpoints/login`, { method: 'POST', body: params, // Envia email, password y vertical }, )
// Si hay error, el estado no es 'success' o falta el JWT, devuelve null if (error.value || status.value !== 'success' || !data.value?.jwt) return null
// Si todo va bien, retorna los datos del usuario (incluye JWT) return { data: data.value, meta: {}, } } catch { // Si ocurre un error inesperado en la petición, retorna null return null }}🧱 Interfaces o tipos utilizados
Section titled “🧱 Interfaces o tipos utilizados”Estas son las interfaces y tipos utilizados en el servicio de inicio de sesión. Hay que fijarse en la estructura del controlador user-login en el proyecto de back para ver qué datos se necesitan consultar en Strapi.
export interface LoginPayload { email: string password: string vertical: string}
export interface LoginResponse { jwt: string user: { id: number email: string nameLastName: string phone?: string documentId?: string origin?: string confirmed?: boolean identificationNumber?: string }}🛠️ Uso en el formulario de Login
Section titled “🛠️ Uso en el formulario de Login”Este es un ejemplo simplificado del uso del servicio de login desde un formulario:
const authStore = useAuthStore() // Acceso a la store de autenticación (Pinia)const { jwt } = storeToRefs(authStore) // Referencia reactiva al JWT almacenado
const { data } = await useLocales<LoginFormLocales>('login') // Carga los textos localizados del formularioconst loginFormLocales = ref(data) // Almacena los textos de forma reactiva
const verticalName = ref(wordCapitalizer(visibleVertical.name)) // Capitaliza el nombre del vertical para enviarlo a StrapiuseInvalidRedirection(['registro', 'iniciar-sesion']) // Evita que usuarios autenticados accedan a esta vista
// Configuración del formulario con validaciones y textosconst { formData: login, // Datos que rellena el usuario invalidFields, // Campos inválidos tras validar validationMessages, // Mensajes de validación} = useForm<LoginData, LoginFormLocales>( { email: '', password_not_empty: '', youre_a_bot: '', }, loginFormLocales,)
// Función que maneja el proceso de loginconst loginProcess = async () => { if (login.youre_a_bot) { // Protección básica contra bots navigateTo('/') return }
try { // Llama al servicio de login con los datos del formulario const result = await useServices<Strapi<LoginResponse>, LoginPayload>('userLogin', { email: login.email, password: login.password_not_empty, vertical: verticalName.value, })
// Si no hay respuesta válida o no hay JWT, muestra un error if (!result || !result.data?.jwt) { showAlert.value = true message.value = loginFormLocales.value?.api_error ?? '' return }
// Guarda el JWT en localStorage y en la store localStorage.setItem('jwt', result.data.jwt) jwt.value = result.data.jwt
// Redirige al usuario a su destino anterior o a la página principal const redirection = localStorage.getItem('redirection') localStorage.removeItem('redirection') navigateTo(redirection ?? '/') } catch { // Captura errores de red u otros imprevistos showAlert.value = true message.value = loginFormLocales.value?.generic_error ?? '' }}</script>🔧 Importacion del servicio en la vertical
Section titled “🔧 Importacion del servicio en la vertical”Asegúrate de registrar el servicio userLogin en cada interfaz de verticals.ts:
interface VerticalsServices { experts: FunctionService<Expert[], FilterParams> expertsBySlug: FunctionService<Expert, FilterParams> partners: FunctionService<Partner[], FilterParams> partnersBySlug: FunctionService<Partner, FilterParams> premium?: FunctionService<PremiumPage> dentalProcedure?: FunctionService<{ products: Product[] }> series: FunctionService<Serie[], FilterParams> seriesBySlug: FunctionService<Serie, FilterParams> episodesBySlug: FunctionService<Episode, Slug> seriesPage: FunctionService<Serie, FilterParams> seriesByCategory: FunctionService<FilteredSeriesByCategory, FilterParams> blogs: FunctionService<Blog[], FilterParams> blogBySlug: FunctionService<Blog, FilterParams> traumaHome?: FunctionService<TraumatologyHomeApi, FilterParams> getHome?: FunctionService<unknown, FilterParams> getLegalPages?: FunctionService<LegalPagesApi, FilterParams> getModuleCategories: FunctionService<CategoriesByModule, ModuleCategoriesParams> inspiriaTalks?: FunctionService<TalksCard[], FilterParams> userRegistration: FunctionService<Strapi<RegisteredUser>, UserRegistrationPayload> // --> userLogin: FunctionService<Strapi<LoginResponse>, LoginPayload> allContents: FunctionService<AllContents[]>}Y luego, en el objeto de configuración de la vertical, importa el servicio:
export interface InspiriaVertical extends Verticals { services: VerticalsServices & { getModuleCategories: FunctionService<CategoriesByModule, ModuleCategoriesParams> dentalProcedure: FunctionService<{ products: Product[] }> inspiriaTalks: FunctionService<TalksCard[], FilterParams> userRegistration: FunctionService<Strapi<RegisteredUser>, UserRegistrationPayload> // --> userLogin: FunctionService<Strapi<LoginResponse>, LoginPayload> getInspiriaLive: FunctionService<InspiriaLive, FilterParams> } zohoConfig?: ZohoConfig}Finalmente, registra el servicio en la constante de la vertical en /constants/nombreVertical.ts:
import { loginUserForVertical } from '@/services/strapi/login'
export const INSPIRIA = { // ... services: { // ... userLogin: loginUserForVertical, },}🛡️ Control de sesión
Section titled “🛡️ Control de sesión”El token jwt se gestiona desde la store de autenticación (auth.ts), donde se implementa un watcher reactivo para detectar cambios en el valor del token y cerrar sesión automáticamente si se detecta manipulación.
Esto asegura un control completo del JWT desde el frontend y elimina la dependencia del módulo @nuxtjs/strapi.
import { PRIVATE_PAGES } from '@/constants/pages'
export const useAuthStore = defineStore('auth', () => { const jwt: Ref<string | null> = ref(localStorage.getItem('jwt')) // Declara una referencia reactiva `jwt` con el valor inicial tomado del localStorage.
if (import.meta.client) { // Solo se ejecuta en el cliente (no en SSR). const storedJwt = localStorage.getItem('jwt') jwt.value = storedJwt // Asegura que `jwt` tenga el valor del localStorage cuando esté en el cliente. }
const user = ref(null) // Crea una referencia reactiva `user`, inicialmente `null`.
const isLoggedIn = computed(() => !!jwt.value) // Computed que devuelve `true` si `jwt` tiene valor, es decir, si el usuario está logueado.
const route = useRoute() // Obtiene la ruta actual del enrutador de Vue.
const logout = () => { // Función que cierra sesión. stopWatcher?.() // Si existe un watcher activo, lo detiene. localStorage.removeItem('jwt') // Elimina el token del localStorage. jwt.value = null user.value = null // Limpia los datos reactivos. startWatcher() // Reinicia el watcher para observar futuros cambios.
const meta = route.matched[0]?.name as string const page = meta.split('___')[0] // Extrae el nombre base de la página actual (antes de `___`, que es usado por Nuxt en i18n).
if (PRIVATE_PAGES.includes(page) && !isLoggedIn.value) { // Si la página es privada y el usuario no está logueado, redirige al home. navigateTo('/') window.location.reload() } }
const checkAuth = async () => { // Comprueba si hay un JWT válido. if (!jwt.value) logout() // Si no hay JWT, llama a logout. }
let stopWatcher: (() => void) | null = null // Variable para guardar la función que detiene el watcher.
const startWatcher = () => { let firstRun = true // Marca si es la primera ejecución del watcher.
stopWatcher = watch( jwt, (newVal) => { // Observa cambios en `jwt`.
if (firstRun) { firstRun = false return // La primera vez que corre el watcher, no hace nada (evita acciones innecesarias). }
if (newVal) { localStorage.setItem('jwt', newVal) } else { localStorage.removeItem('jwt') } // Sincroniza `jwt` con el localStorage.
logout() // Si el valor cambia (manualmente o por otro tab), hace logout. }, { flush: 'post' }, // Se ejecuta después del render (para evitar bucles o errores). ) }
if (import.meta.client) { startWatcher() // Inicia el watcher solo en cliente.
window.addEventListener('storage', (event) => { // Escucha eventos de `storage` para detectar cambios del JWT en otras pestañas. if (event.key === 'jwt') { const newValue = event.newValue if (newValue !== jwt.value) { logout() // Si el JWT cambia en otra pestaña, fuerza logout en esta. } } }) }
return { isLoggedIn, checkAuth, logout, jwt, user, } as { // Retorna las propiedades y funciones de la store, tipadas explícitamente. isLoggedIn: typeof isLoggedIn checkAuth: () => Promise<void> logout: () => void jwt: Ref<string | null> user: Ref<any> }})Mediante el middleware auth.global.ts nos aseguramos de que se produzca la redirección a / si el usuario no ha iniciado sesión e intenta acceder a una página privada:
import { usePage } from '@/composables/usePage'export default defineNuxtRouteMiddleware(async (to) => { if (import.meta.server) return
const authStore = useAuthStore()
const { isLoggedIn } = storeToRefs(authStore) const { getPageMetaName } = usePage() const adminRoutes = ['account']
const meta = to.matched[0].name const page = getPageMetaName(meta as string)
if (adminRoutes.includes(page) && !isLoggedIn.value) { return navigateTo('/') }})Este flujo permite una autenticación personalizada y controlada desde Nuxt, compatible con Strapi y adaptada a una arquitectura multi-vertical.