Simulando un Login
Antes que nada una advertencia importante:
Lo que voy a explicar a continuación no es un Login real, es solamente un ejercicio para practicar la lógica de comunicación entre componentes en Vue, y una primera aproximación al tema de la interacción entre frontend y backend.
Para hacer un Login real tendríamos que crear nuestro propio backend (por ejemplo, con Node.js, Express, y Passport) o habría que usar algún Backend as a Service como Firebase, Supabase o AWS.
En otro posteo voy a explicar cómo se hace eso, pero para este ejemplo voy a usar MockApi, que es como una versión muy simplificada de un Backend as a Service.
MockApi
A pesar de su simplicidad, MockApi tiene muchas similitudes con un BaaS real, como la posibilidad de crear Schemas de datos (ya vamos a ver qué es eso), guardar información, modificarla o borrarla, e incluso hacer queries simples (ya vamos a ver qué es eso también).
La comunicación entre el frontend y MockApi se puede hacer usando fetch , el método nativo de JavaScript para peticiones HTTP, ya sea para guardar datos (POST), modificarlos (PUT), borrarlos (DELETE), o simplemente obtenerlos (GET), para lo cual lo único que se necesita es tener la URL de nuestro endpoint. El endpoint es la ruta a la que se accede para interactuar con algún recurso del backend:
const mockApiUrl = 'https://123456789123456789.mockapi.io/api'
// En este caso sería el endpoint de productos:
const endpoint = '/productos'
const url = mockApiUrl + endpoint
export default {
data: () => ({
products: []
}),
methdods: {
async getData(url) {
try {
this.products = await (await fetch(url)).json()
} catch (error) {
console.log(error)
}
}
}
}
Poder hacerlo de esta forma (sin necesidad de tokens ni API Keys de autenticación) es una gran ventaja pero también una gran vulnerabilidad, porque cualquiera que tenga la URL del endpoint puede acceder a nuestros datos en MockApi. De todas formas, como primera aproximación al funcionamiento de una aplicación desacoplada MockApi es una muy buena opción.
Configurando MockApi
Primero tienen que registrarse en MockApi (pueden hacerlo con su cuenta de GitHub) y crear un proyecto:
Y luego le agregan un prefix (o sea, el directorio raíz de los distintos endpoints). La convención es llamarlo /api:
Ahí les va a generar el primer endpoint:
Luego clickean en New Resource para generar un recurso dentro de la base de datos. En este caso lo pueden llamar users:
Les va a dar algunas opciones de datos (Schema) que se van a crear para cada usuario: id, createdAt, name, avatar. Van a ver que al lado dice FAKER.JS. Eso significa que esos datos van a ser creados automáticamente usando Faker.js, una librería para generación de datos random de prueba.
Esto es muy útil para generar las imágenes de avatar de los usuarios en forma automática (con imágenes random que provee Faker) sin que tengamos que implementar nosotros mismos la carga de la imagen, lo cual implicaría tener que configurar un bucket en el backend y un widget en el frontend (en otro posteo voy a explicar cómo hacer eso 📝️).
id y createdAt también son útiles, porque generan automáticamente el id y la fecha de creación de cada nuevo usuario. Pero name lo pueden borrar ya que ese dato lo vamos a enviar nosotros desde el frontend, llamándolo username, y va a ser de tipo String (no un dato random de Faker.js):
Además de username podemos agregar otros campos como firstname, lastname, email y password (todos de tipo String):
Luego le dan Create y listo, ya tienen su resource de usuarios:
Luego clickean en el botón que dice data y van a ver un array vacío. Dentro de ese array pueden copiar algunos datos en formato JSON como para tener un usuario de testeo:
[
{
"id": "1",
"firstname": "Linus",
"lastname": "Octocat",
"username": "user1",
"password": "test123",
"email": "linus@git.com",
"avatar": "https://avatars.githubusercontent.com/u/583231?v=4",
"admin": true
}
]
Como ven, MockApi es muy fácil de configurar en comparación con otros BaaS como Firebase o Supabase.
El componente de Login
Para crear el componente de Login se puede reutilizar el ejemplo del formulario que está acá, sólo que en este caso en lugar de los inputs nombre, apellido, e-mail y password tendríamos únicamente usuario y password.
En el componente de la NavBar tenemos que incluir un botón de Login (o un ícono) que al clickearlo nos muestre el formulario. Para mostrar el formulario de Login hay distintas posibilidades:
1. Vue Router: Crear una view de Login y redireccionar al usuario a esa view cuando clickea el botón:
methods: {
goToLoginPage() {
this.$router.push('/login')
}
}
El problema con esto es que primero tendríamos que configurar Vue Router. Pero ése es un tema demasiado largo como para ver en un sólo posteo. Quedará para otro momento 📝️
2. Simular un router: Una opción más simple es simular el comportamiento de un router (pero con muchas más limitaciones que un router real). Acá pueden ver un ejemplo que muestra cómo sería eso.
3. Ventana modal: En el post sobre cómo hacer una modal pueden encontrar las instrucciones. Lo único que tienen que hacer es pasarle el componente del formulario al componente de la modal mediante slots. Creo que esta última opción es la más simple.
El componente de Login por ahora quedaría así:
<template>
<div>
<UsernameInput/>
<PasswordInput/>
<button @click="">INGRESAR</button>
<p>
¿Aún no estás registrado? Registrate
<a href="" @click.prevent="">aquí.</a>
</p>
</div>
</template>
<script>
import UsernameInput from '@/components/user/inputs/UsernameInput.vue'
import PasswordInput from '@/components/user/inputs/PasswordInput.vue'
export default {
components: {
UsernameInput,
PasswordInput
}
}
</script>
Recuerden que para evitar que se recargue la página al clickear en el anchor tag (<a>
) deben usar @click.prevent
Stores
Cada vez que un usuario se loggea a la aplicación el frontend necesita tener acceso a la data del usuario en forma global, porque hay varios componentes que van a necesitar usarla:
1. El componente de Login desde donde se va a ejecutar el método que comunique el nombre de usuario y password al backend y muestre un mensaje de error si alguno de estos datos es incorrecto.
2. El componente de Signup desde donde se va a manejar la lógica del registro del usuario.
3. La NavBar para ocultar el botón de Login y mostrarle al usuario que está loggeado, ya sea con un texto mostrando su nombre o, más usualmente, mostrando su imagen de avatar:
4. El carrito, para avisarle al usuario que antes de poder completar la compra debe loggearse:
5. Las cards de los productos. Por ejemplo, si el usuario le da like a un producto habría que poder guardar ese like en el backend identificado con el id del usuario loggeado (para saber que fue ese usuario y no otro el que le dio like al producto).
6. El componente del perfil del usuario, donde se le muestra toda la información que tiene cargada.
7. La sección Mis pedidos, en la que se muestren todas las compras que hizo el usuario.
Para que todos estos componentes puedan acceder a la información del usuario en forma sincronizada es necesario usar una herramienta de administración de estado global como Vuex o Pinia. El uso de estas herramientas es un tema largo que quedará para otro posteo 📝️
Mientras tanto podemos usar una store simple, como el que está acá.
La store del usuario a su vez necesita acceder a la store del formulario (formStore) como el que vimos en este ejemplo. La store del formulario sería como esa, pero además le agregamos el objeto serverErrors (o también podría llamarse backendErrors) para los mensajes de error al chequear los datos con el backend.
Y le borramos el array de usuarios que teníamos en ese ejemplo, porque en este caso no vamos a estar guardando todos los usuarios sino sólo uno, el que se loggea:
import {
Validation,
EmailValidation,
PasswordValidation,
UsernameValidation
} from './validationClass'
export const formStore = {
form: {
firstname: '',
lastname: '',
username: '',
password: '',
email: ''
},
submitted: false,
loading: false,
serverErrors: {
wrongUsername: '',
wrongPassword: '',
takenUsername: '',
takenPassword: ''
},
// Validators
firstnameErrors() {
return new Validation(this.form.firstname, this.submitted)
},
lastnameErrors() {
return new Validation(this.form.lastname, this.submitted)
},
usernameErrors() {
return new UsernameValidation(this.form.username, this.submitted)
},
emailErrors() {
return new EmailValidation(this.form.email, this.submitted)
},
passwordErrors() {
return new PasswordValidation(this.form.password, this.submitted)
},
// Antes del Signup chequear os campos del formulario
checkSignup() {
return (
this.firstnameErrors().noErrors() &&
this.lastnameErrors().noErrors() &&
this.usernameErrors().noUsernameErrors() &&
this.emailErrors().noEmailErrors() &&
this.passwordErrors().noPasswordErrors()
)
},
// Antes del Login chequear los campos del formulario
checkLogin() {
return (
!this.usernameErrors().emptyField() &&
!this.passwordErrors().emptyField()
)
},
// SETTER para v-model
setNewValue(field, newValue) {
this.form[field] = newValue
},
resetForm() {
// Resetear el formulario:
Object.keys(this.form).forEach(key => this.form[key] = '')
this.resetServerErrors()
this.submitted = this.loading = false
},
// Resetear el objeto serverErrors:
resetServerErrors() {
Object.keys(this.serverErrors).forEach(key => this.serverErrors[key] = '')
}
}
Muchas de estas validaciones no las vamos a usar en el Login, pero las dejamos así porque más tarde las vamos a necesitar para el formulario de registro de un nuevo usuario (Signup).
Store del usuario
Para poder acceder a las propiedades y métodos de la store del formulario primero hay que importarla en la store del usuario y declararla como una propiedad de ésta:
// Importar la store del formulario
import { formStore } from './formStore'
export const userStore = {
// Store del formulario dentro de la store del usuario:
formStore,
// STATE
user: null,
// GETTERS
get loggedIn() {
},
// SETTERS
loginUser() {
},
signupUser() {
},
signoutUser() {
}
}
Al comienzo user va a ser null y luego, cuando el usuario esté loggeado, va a ser un objeto que contenga la data del usuario.
El getter loggedIn debe retornar un booleano. Si user es null que retorne false
y si user es un objeto con alguna propiedad dentro, que retorne true
.
Pero, ¿cómo hacer que la existencia de propiedades en un objeto dé como resultado true
? Una forma posible sería usando el método nativo de JavaScript Object.keys():
get loggedIn() {
return Object.keys(this.user).length
}
Object.keys() retorna un array con los nombres de las propiedades del objeto, y si este array tiene un largo (length) quiere decir que el objeto tiene propiedades y por lo tanto que el usuario está loggeado.
Pero hay una forma más fácil de hacer esto: con la doble negación (que en JavaScript es !!
):
get loggedIn() {
return !!this.user
}
La doble negación funciona así: si user es un objeto, su contrario, o sea !this.user
, sería false
. Y como el contrario de false
es true
, al negar la negación el objeto termina retornando true
.
Fíjense que en este caso estoy usando el operador get de JavaScript. Esto es para simplificar la sintaxis y para que se vea claramente que se trata de un getter. De esta forma el getter pasa a funcionar como una propiedad más del objeto, no como un método. Entonces, al accederla puede hacerse sin los paréntesis:
if (this.userStore.loggedIn) {
this.showLoginModal = false
}
Para los setters (loginUser, signupUser, signoutUser) vamos a necesitar conectarnos con el backend usando fetch
. Lo más recomendable sería tener la lógica del fetch
por fuera de la store, así esta lógica puede ser accedida por cualquier componente o cualquier store que la necesite.
Fetch service
Hay distintas formas de usar fetch
, algunas más simples, otras más complejas. Vamos a empezar por una forma relativamente simple, usando la sintaxis para promesas con async/await
, que suele ser la más usada en Vue, como pueden ver en los ejemplos de la documetación.
Dentro de la carpeta src creamos otra llamada services. Esto es porque la convención es llamar así a los servicios de acceso al backend. Y dentro de services un archivo que se puede llamar fetchService.js:
// src/services/fetchService.js
export const fetchService = {
async getData(url) {
try {
const res = await (await fetch(url)).json()
return res
} catch (error) {
console.log(error)
}
}
}
Variables de entorno
Aunque MockApi tenga muchas limitaciones en cuanto a la seguridad, sería bueno intentar mitigar su vulnerabilidad al menos tratando de no exponer nuestro token de MockApi en el repositorio. El token es el número que figura en la URL antes de .mockapi.io
:
https://aca-va-el-token.mockapi.io/api/users
Además, si exponen el token es muy posible que reciban un mail del bot de GitHub alertándolos al respecto:
GitHub Bot ⚠️
Your repository has security vulnerabilities!
Exponer los tokens (o las API keys) en un repositorio es considerado una mala práctica. Esto es porque hay bots que recorren los repositorios de GitHub recolectando este tipo de información, y luego nuestra cuenta puede ser usada por algún hacker 😬️ Aunque lo que estamos haciendo sea sólo un ejemplo y no un Login real vamos a intentar hacerlo lo mejor posible.
Para no exponer el token podemos guardar la URL de MockApi en una variable de entorno.
En la raíz del proyecto deben crear un archivo .env
e incluir lo siguiente:
VITE_MOCKAPI_URL=https://aca-va-el-token.mockapi.io/api
La convención es nombrar a las variables de entorno en mayúsculas 🤷♂️️ Y no usar comillas para el valor de la variable.
Y recuerden que si instalaron Vue con Vite todas las variables de entorno deben comenzar con el prefijo VITE_
, y luego son accedidas desde cualquier componente de esta forma:
const url = import.meta.env.VITE_VARIABLE
O usando destructuring:
const { VITE_VARIABLE: variable } = import.meta.env
Como ven, en el archivo .env
le saqué a la URL la parte de /users
. Esto es por si luego queremos usar esta misma URL para otro endpoint (/products
, por ejemplo). La parte de /users
se la podemos agregar después, cuando necesitemos acceder a ese endpoint.
.gitignore
No olviden agregar .env
dentro de .gitignore
para que el archivo .env
no sea incluido en los commits al repositorio.
También es considerado una buena práctica crear en la raíz del proyecto un archivo que se llame .env.example
que sirve como guía para otros desarrolladores que estén trabajando en el proyecto y necesiten saber qué variables de entorno se están usando:
# completar el valor de la variable:
VITE_MOCKAPI_URL=
Aquí se pone únicamente el nombre de la variable, sin su valor. A diferencia del archivo .env
, el archivo .env.example
sí debe ser incluido en el commit a GitHub, no debe estar en .gitignore
. Luego, quien haga un fork o un clone del repo debe completar el valor de la variable y renombrar el archivo .env.example
como .env
.
Recuerden también que esto es sólo para no exponer las variables de entorno en el repositorio, pero una vez que la app está online cualquiera que tenga cierto conocimiento de JavaScript podría llegar a encontrar estas variables usando las Dev Tools del browser. Para evitar esto habría que usar funciones serverless que hagan de intermediarias entre el frontend y MockApi, y usar la URL de MockApi únicamente ahí. Pero el tema de las serverless functions es largo y merece un post aparte 📝️
Buscar el nombre de usuario
En el componente de Login tenemos sólo dos inputs: username y password. Cuando el usuario ingresa estos datos lo primero que tenemos que hacer es verificar si existe un usuario con ese nombre, y si no existe mostrarle al usuario un mensaje de error (serverErrors.wrongUsername) para que revise si lo escribió bien, o que haga un Signup si aún no está registrado.
Para verificar si el usuario existe hay dos formas: la muy mala y la no tan mala (lamentablemente, la buena no es posible con MockApi y quedará pendiente para el posteo sobre Vue + Supabase 📝️).
Empecemos por la muy mala, sólo para entender por qué es tan mala.
Una muy mala idea
En la store del usuario creamos el método loginUser que va a hacer uso del método getData(url) del objeto fetchService (que dentro de la store del usuario vamos a renombrar como userService).
Con getData(url) hacemos un fetch al endpoint de usuarios en MockApi, guardamos la lista completa de usuarios en el array users y con find
buscamos si en ese array hay un usuario con un nombre que coincida con el ingresado en el input:
const { VITE_MOCKAPI_URL: baseUrl } = import.meta.env
import { fetchService as userService } from '@/services/fetchService'
import { formStore } from './formStore'
export const userStore = {
formStore,
// STATE
user: null,
// GETTERS
get loggedIn() {
return !!this.user
},
// SETTERS
async loginUser() {
this.formStore.submitted = true
// Chequear primero si los campos fueron completados:
if (this.formStore.checkLogin()) {
const usersEndpoint = baseUrl + '/users'
// fetch al endpoint de usuarios en MockApi
const users = await userService.getData(usersEndpoint)
// Buscar dentro del array de usuarios un usuario
// con el mismo nombre ingresado en el formulario:
const user = users.find(user =>
this.formStore.form.username === user.username)
// Chequear si el password ingresado es el mismo que está guardado en MockApi
// Si no encuentra nada, el método find retorna undefined.
// Por lo tanto hay que usar Optional Chaining:
// Si user es undefined, user?.password también lo es
// pero sin arrojar un error por esto.
const checkPassword = (
this.formStore.form.password === user?.password
)
if (!user) {
this.formStore.serverErrors.username =
'Usuario no registrado.'
} else if (!checkPassword) {
this.formStore.serverErrors.password =
'Password inválido.'
} else {
// Si el usuario existe y el password es correcto
// guardarlo en la store
this.user = user
this.formStore.resetForm()
}
}
}
}
Funciona, pero hay un problema: dentro del array users
estamos guardando todos los usuarios guardados en el backend para luego buscar el usuario ingresado dentro del array. Entonces, al tener los datos de todos los usuarios en ese array, cualquiera con un mínimo conocimientos de JavaScript y acceso a las Dev Tools del browser podría obtener los passwords de todos los usuarios que usen la aplicación 😱️
Una idea no tan mala
Afortunadamente MockApi tiene la posibilidad de aceptar queries, es decir, pedirle a MockApi que busque dentro de su base de datos si encuentra algún dato que cumpla con cierta condición.
Según la documentación de MockApi para hacer un query hay que crear un objeto URL pasándole como parámetro el endpoint y luego adjuntarle los search params como dos strings, el primero la propiedad y el segundo el valor a buscar:
const url = new URL('https://PROJECT_TOKEN.mockapi.io/users/1/tasks');
url.searchParams.append('title', 'hello');
En realidad no es necesario hacerlo tan "verboso", se puede hacer más fácil usando template literals y la sintaxis para queries de HTTP, en la que luego del endpoint se pone un signo de interrogación, luego la prop, luego un signo de igual, y luego el valor a buscar:
const userQuery = `${baseUrl}/users?username=${this.formStore.form.username}`
// Query al endpoint de usuarios en MockApi
// Esta búsqueda retorna un solo usuario que tenga ese nombre:
const response = await userService.getData(userQuery)
// Aunque sea un solo usuario, la respuesta de MockApi siempre es un array
// Y si no encontró nada, es un array vacío
// en cuyo caso response[0] es undefined
const user = response[0]
De esta forma evitamos tener que guardar la lista completa de usuarios en el frontend.
No es que esto sea lo más sofisticado en materia de ciberseguridad... Alguien con conocimientos de JavaScript podría cambiar el valor del endpoint para que quede en /users
en vez de /users?username=${this.formStore.form.username}
y estamos en la misma...
Pero al menos sirve para que quede claro el concepto: el frontend nunca debe recibir del backend más información de la que realmente necesita.
Por otro lado, deben tener en cuenta que las queries a MockAPI funcionan como filters, es decir, devuelven todas las coincidencias, incluso las coincidencias parciales (por ejemplo, si el username registrado es user1 y el usuario ingresa solamente user MockAPI devuelve los datos de user1 🤦♂️️). Si tuviésemos control sobre nuestro backend esto podría ser evitado. Pero bueno, no se le puede pedir más a MockAPI...
Otra muy mala idea
En una aplicación real el chequeo del password jamás se haría en el frontend:
// Esto nunca se hace en el Frontend
const checkPassword = this.formStore.form.password === user?.password
¿Por qué? Porque fácilmente alguien que sepa un poco de JavaScript podría entrar a las Dev Tools del browser y modificar este chequeo:
const checkPassword = true // 😠️
Y de esta forma cualquier password que ingrese va a ser chequeado como correcto... 🤦♂️️
Entonces, ¿cómo solucionar esto? Hay que hacer el chequeo del password en el backend. El problema es que MockApi no tiene esta opción. Nuevamente, la solución sería usar funciones serverless.
Mientras tanto vamos a dejarlo así, pero siempre recordando que más adelante debemos sacar este chequeo del frontend 📝️
Encriptación del password
TL;DR:
Esta parte es medio enroscada y si les aburre se la pueden saltear. Básicamente se trata de que los passwords siempre tienen que estar hasheados.
Nuestro Login simple no es muy seguro que digamos pero como dijimos, vamos a tratar de hacerlo lo más parecido a un Login real, al menos para entender en qué se diferencia de éste.
Por ejemplo, en un backend real los passwords jamás estarían guardados en la base de datos tal como los ingresó el usuario, o sea como plaintext, siempre estarían encriptados (o, mejor dicho, hasheados). De esta forma, ni siquiera el Admin de la base de datos puede saber los passwords reales de los usuarios.
La diferencia entre encriptación y hashing es que la encriptación es reversible (mediante la desencriptación, claro) mientras que el hashing no. Entonces, al guardarse el hash en lugar del plaintext nadie que tenga acceso a la base de datos (un hacker o el mismo Admin de la db) puede saber los passwords reales en forma directa.
Lo que un hacker podría hacer es comparar una larga lista de hashes derivados de los passwords más usados ('qwerty123', etc) con los hashes en la db, sobre todo aquellos hashes que aparecen repetidas veces (lo cual indica que derivan de passwords muy comunes).
Para mitigar este riesgo lo que se suele hacer es concatenar un string aleatorio (llamado salt) al password ingresado por el usuario de manera que, aunque el usuario use un password muy común, éste sea mucho más difícil de crackear. Además, al ser aleatorio el salt hace que dos passwords iguales produzcan dos hashes distintos.
Incluso de esta forma, si se cuenta con la suficiente velocidad de procesamiento, los passwords salteados podrían ser crackeados, por lo que se agrega un paso adicional llamado rounds por el cual el hash es re-hasheado una cierta cantidad de veces, lo cual hace que los ataques sean más costosos por la magnitud de recursos que se necesitan para crackear los passwords.
Para hacer todo esto (hashing, salting, rounds) lo que más se suele usar es el algoritmo bcrypt. La versión JavaScript de este algoritmo se puede encontrar en la librería de npm bcrypt.js. Pero si están usando Vue 3, esa versión no les va a funcionar (sólo funciona si están usando Vue 2). Para Vue 3 deben instalar brcrypt-ts.
Generando el hash
Como todavía no hicimos el componente de Signup podemos obtener el hash del password del usuario de prueba en un console.log
, luego copiamos el hash, y luego en MockApi reemplazamos manualmente el password en plaintext (que en el ejemplo de acá arriba era 'test123') por el hash.
Para eso, al comienzo del script del componente App.vue incluímos lo siguiente:
import { hashSync } from 'bcrypt-ts/browser'
const password = 'test123' // o el password que estén usando para el usuario de prueba
// 10 es el exponente de la cantidad de rounds
const hash = hashSync(password, 10)
console.log({hash})
Luego hacemos correr la aplicación con npm run dev
y en la consola del browser vamos a ver el hash:
Object { hash: "$2a$08$LOLQuo8PCyuIOAnlSGuUhelsIiSOcCFFepC9CM/ZpywWrCbqNb7VS" }
Vamos al resource de users en MockApi, abrimos data y en el campo password del usuario de prueba reemplazamos el plaintext por el hash:
[
{
"id": "1",
"firstname": "Linus",
"lastname": "Octocat",
"username": "user1",
"password": "$2a$08$LOLQuo8PCyuIOAnlSGuUhelsIiSOcCFFepC9CM/ZpywWrCbqNb7VS",
"email": "linus@git.com",
"avatar": "https://avatars.githubusercontent.com/u/583231?v=4",
"admin": true
}
]
Una vez que tenemos el hash podemos borrar esas 4 líneas de App.vue. Y al comienzo del archivo userStore.js importamos las funciones hashSync y compareSync de bcrypt-ts. La función compareSync sirve para generar un hash a partir del password ingresado y compararlo con el hash que proviene de la db. Si encuentra una coincidencia retorna true
.
Usando brcypt en el método de login
Y ahora ya podemos incorporar la función de hasheado al método de login en userStore:
const { VITE_MOCKAPI_URL: baseUrl } = import.meta.env
import { hashSync, compareSync } from 'bcrypt-ts/browser'
import { fetchService as userService } from '@/services/fetchService'
import { formStore } from './formStore'
export const userStore = {
formStore,
user: null,
get loggedIn() {
return !!this.user
},
async loginUser() {
this.formStore.submitted = true
if (this.formStore.checkLogin()) {
const userQuery = `${baseUrl}/users?username=${this.formStore.form.username}`
const response = await userService.getData(userQuery)
const user = response[0]
// Usar la función de comparación con hasheo de bcrypt-ts
const checkPassword = user && compareSync(
this.formStore.form.password, user.password
)
if (!user) {
this.formStore.serverErrors.username =
'Usuario no registrado.'
} else if (!checkPassword) {
this.formStore.serverErrors.password =
'Password inválido.'
} else {
this.user = user
this.formStore.resetForm()
}
}
}
}
Ahora nuestra función de login está un poco mejor, aunque aún no solucionamos el problema de estar chequeando el password en el frontend. Pero como dijimos, para solucionar esto habría que pasar este chequeo a una función serverless y este post ya está quedando demasiado largo... 😑️
Sin embargo, debemos recordar que esto es algo que más adelante vamos a tener que hacer 📝️
Inlcuir el método loginUser en el componente
Retomando: si el usuario existe y el password es correcto, la data del usuario es guardada en la propiedad user de userStore, y el getter loggedIn retornará true
.
Entonces, ya podemos incluir el método loginUser() en el componente de Login:
<template>
<div>
<UsernameInput/>
<PasswordInput/>
<button @click="loginUser">INGRESAR</button>
<p>
¿Aún no estás registrado? Registrate
<a href="" @click.prevent="goToSignup">aquí.</a>
</p>
</div>
</template>
<script>
import UsernameInput from '@/components/user/inputs/UsernameInput.vue'
import PasswordInput from '@/components/user/inputs/PasswordInput.vue'
import { userStore } from '@/stores/userStore'
import { formStore } from '@/stores/formStore'
export default {
components: {
UsernameInput,
PasswordInput
},
data: () => ({
userStore,
formStore
}),
methods: {
async loginUser() {
await this.userStore.loginUser()
if (this.userStore.loggedIn) {
// Si el Login se muestra en una ventana modal, cerrarla:
this.$emit('close-modal')
// Si se muestra simulando un router, ocultar Login y mostrar Home:
// this.showPage('home')
// Si se muestra en una view, redirigir a Home:
// this.$router.push('/')
}
},
goToSignup() {
this.formStore.resetForm()
this.$emit('show-signup')
// Si el componente de Signup se muestra simulando un router
// ocultar Login y mostrar Signup:
// this.showPage('signup')
// Si se muestra en una view, redirigir a Signup:
// this.$router.push('/signup')
}
}
}
</script>
Como toda la lógica del Login está en userStore lo único que tiene que hacer el método en el componente es llamar a this.userStore.loginUser() en forma asincrónica y luego cerrar la modal de Login (o redirigir a la ruta principal si están usando un router).
Mostrar la imagen de avatar
Una vez que el usuario está loggeado, podemos mostrar en el componente NavBar algún mensaje de bienvenida (Hola nombre-de-usuario!
) o, mejor, su imagen de avatar:
<template>
<nav>
<h1 @click="goToHome">Vue Bakery</h1>
<CartCounter/>
<div @click="goToLogin">
<UserIcon v-if="!userStore.loggedIn"/>
<UserAvatar :imgsrc="imgsrc" v-else/>
</div>
</nav>
</template>
<script>
import CartCounter from '@/components/cart/CartCounter.vue'
import UserIcon from '@/components/icons/UserIcon.vue'
import UserAvatar from '@/components/user/UserAvatar.vue'
import { userStore } from '@/stores/userStore'
export default {
components: {
CartCounter, UserIcon, UserAvatar
},
data: () => ({
userStore
}),
computed: {
imgsrc() {
// Usar Optional Chaining, de lo contrario da error
// si no hay un usuario loggeado:
return this.userStore?.user?.avatar
}
},
methods: {
goToLogin() {
// Si no hay un usuario loggeado
// al clickear en el ícono se muestra la modal de Login
if (!this.userStore.loggedIn) {
this.$emit('show-login')
}
},
goToHome() {
this.$emit('show-home')
}
}
}
</script>
Si el usuario no está loggeado, se muestra el ícono:
Y una vez que se loggea, se muestra su imagen de avatar:
Confirmar compra
Al final del tutorial sobre el carrito de compras decíamos que antes de confirmar la compra había que chequear si el usuario estaba loggeado, y si no lo estaba, mostrarle un mensaje pidiendo que ingrese:
<p v-show="showLoginMessage">
Antes de confirmar la compra debes ingresar ⬆️
</p>
<button @click="confirmOrder">
CONFIRMAR COMPRA
</button>
Ahora que ya tenemos implementado el Login podemos agregar esta funcionalidad al componente del carrito:
import CartTable from '@/components/cart/CartTable.vue'
import { cartStore } from '@/stores/cartStore'
import { userStore } from '@/stores/userStore'
export default {
components: {
CartTable
},
data: () => ({
cartStore,
userStore,
showLoginMessage: false
}),
methods: {
confirmOrder() {
if (this.userStore.loggedIn) {
this.$emit('close-cart')
this.cartStore.emptyCart()
} else {
this.showLoginMessage = true
}
}
}
}
Si no hay un usuario loggeado se muestra el mensaje, y si lo hay, se cierra la modal y se vacía el carrito.
Por ahora lo podemos dejar así, aunque más adelante habría que agregar la funcionalidad de guardar en el backend la orden de compra antes de vaciar el carrito. Y también agregar una sección "Mis pedidos" donde se muestren todas las compras que hizo el usuario.
Logout
El Logout es muy simple: solamente tenemos que agregar en userStore un método que borre el objeto guardado en user y que éste vuelva a ser null:
logoutUser() {
this.user = null
}
Y en la NavBar un botón que al clickearlo dispare este método (recuerden que el botón de Logout sólo debe mostrarse si hay un usuario loggeado):
<template>
<button v-if="userStore.loggedIn" @click="logoutUser">
Logout
</button>
</template>
<script>
import { userStore } from '@/stores/userStore'
export default {
data: () => ({
userStore
}),
methods: {
logoutUser() {
if (this.userStore.loggedIn) {
this.userStore.logoutUser()
}
}
}
}
</script>
Y al settear user en null el getter loggedIn va a retornar false
, y a su vez todo lo que en la aplicación dependa del valor de loggedIn (imagen de avatar, funcionamiento del botón de Login y Logout, mensaje en el carrito, etc) va a cambiar automáticamente gracias al sistema de reactividad de Vue 🥳️
Ejemplo
Ahora faltaría agregar la funcionalidad para el registro de nuevos usuarios (Signup). Como este post sobre el Login ya se hizo demasiado largo voy a dejar el de Signup para un post aparte 📝️
Mientras tanto, acá pueden ver un ejemplo funcional de todo esto que vimos.