Skip to content

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.


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.


/services/strapi/login.ts


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
}
}

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
}
}

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 formulario
const loginFormLocales = ref(data) // Almacena los textos de forma reactiva
const verticalName = ref(wordCapitalizer(visibleVertical.name)) // Capitaliza el nombre del vertical para enviarlo a Strapi
useInvalidRedirection(['registro', 'iniciar-sesion']) // Evita que usuarios autenticados accedan a esta vista
// Configuración del formulario con validaciones y textos
const {
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 login
const 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,
},
}

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.