Introducción

Hoy toca un tutorial 100% práctico. Vamos a construir un ejemplo práctico de login creado con Angular. Este tutorial te va a venir muy bien para afianzar los conocimientos que ya conoces.

Es muy IMPORTANTE que tengas muy claro cómo se hacen llamadas a una API desde Angular. Si no sabes hacerlo o no lo recuerdas te recomiendo que eches un vistazo al artículo anterior: Servicios y llamadas HTTP en Angular

Qué es un sistema de autenticación con token JWT

Antes de empezar vamos a ver rápidamente que es esto de JWT ya que es el sistema de autenticación que vamos a usar.

JWT es un sistema de autenticación que se basa en el uso de tokens. El mecanismo es el siguiente, el usuario hace login en la página y Angular envía al servidor el nombre de usuario o email y su contraseña. El servidor comprueba si el usuario existe en la base de datos y si la contraseña es correcta y envía a la página web un token generado específicamente para ese usuario. Ese token está firmado por lo que no se puede modificar. La página web guarda ese token y cuando el usuario accede a otra página o ejecuta una acción en la página que dependa del servidor envía el token almacenado.

En resumidas cuentas, se genera un token en servidor, la página se lo guarda en la cookie o en memoria y por cada petición a la API se envía el token para que el servidor compruebe si el usuario tiene permisos. De esta forma se consigue que se puedan proteger llamadas a la API sin necesidad de pasar usuario y contraseña en cada petición (solo se pasa el token).

El servidor genera el token y la página lo usa en cada petición

🗺️ Hoja de ruta

  • [ ] 🏗️ Crear y maquetar las vistas de login y registro con formularios
  • [ ] ✨ Conectar el sistema de login y registro con la API
  • [ ] ✨ Recordar la sesión iniciada al recargar la página

Maquetación de los formularios

Vamos a empezar maquetando los formularios pero todavía no los vamos a conectar a la API. Simplemente vamos a crear las vistas con los inputs para que el usuario pueda escribir y el botón de login y registro. También vamos a dejar preparada la lógica de conectar los inputs en la vista con el componente para luego poder enviar los datos a la API.

Empecemos generando un proyecto vacío de Angular. También puedes usar uno que ya tengas generado. Puedes llamarlo como quieras.

ng new ejemplologin
cd ejemplologin
npm install

En este punto yo suelo hacer git commit porque me gusta tener un commit con el proyecto generado vacío, por lo que pueda pasar.

Ahora toca añadir el router. No voy a explicar mucho esto porque ya lo dejé explicado en este artículo: Cómo crear y usar el router de Angular.

Mi app.routing.ts lo he dejado así:

// app.routing.ts

import { RouterModule } from "@angular/router";
import { AppComponent } from "./app.component";
import { LoginComponent } from "./login/login.component";
import { RegisterComponent } from "./register/register.component";

const appRoutes = [
  { path: "", component: AppComponent, pathMatch: "full" },
  { path: "login", component: LoginComponent, pathMatch: "full" },
  { path: "register", component: RegisterComponent, pathMatch: "full" }
];
export const routing = RouterModule.forRoot(appRoutes);

La idea es tener 3 rutas: La página principal, la del login, y la del register y cada ruta la controla su propio componente. Si te sale error es porque has definido rutas en este fichero de rutas pero todavía no has creado los componentes en sí. Toca crear los dos componentes, uno dentro de la carpeta app/login y otro dentro de app/register.

La estructura quedaría así:

La imagen muestra la estructura de carpetas antes comentada

El archivo app.module.ts queda así:

// app.module.ts

import { routing } from "./app.routing";
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppComponent } from "./app.component";
import { LoginComponent } from "./login/login.component";
import { RegisterComponent } from "./register/register.component";

@NgModule({
  declarations: [AppComponent, LoginComponent, RegisterComponent],
  imports: [BrowserModule, routing],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Y en el app.component.html he puesto solo esto:

<!-- app.component.html -->
<div>
  <router-outlet></router-outlet>
</div>

Y en el app.component.css he eliminado todo para que los componentes de login y register no hereden esos estilos.

Para empezar vamos a modificar el componente de login.component.html. Aquí simplemente lo que hago es crear un formulario con inputs para que el usuario pueda escribir su email y su contraseña para poder hacer login y register. Ambos componentes son muy parecidos.

El archivo login.component.html yo lo he puesto así:

<!-- login.component.html -->

<div class="login">
  <form class="custom-form" method="post">
    <h1>Login</h1>
    <input
      type="email"
      [(ngModel)]="email"
      name="email"
      placeholder="Email"
      required="required"
    />
    <input
      type="password"
      [(ngModel)]="password"
      name="password"
      placeholder="Password"
      required="required"
    />
    <button type="submit" (click)="login()">Log in</button>
  </form>
</div>

Un simple formulario con dos inputs. Cada input se bindea a una variable definida dentro del archivo login.component.ts mediante el ngModel. También se bindea el evento de click del botón para detectar cuando el usuario lo pulsa.

Dentro del componente, en el fichero login.component.ts se crea algo de este estilo:

// login.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-login",
  templateUrl: ["./login.component.html"],
  styleUrls: ["./login.component.css"]
})
export class LoginComponent {
  email: string;
  password: string;

  constructor() {}

  login() {
    console.log(this.email);
    console.log(this.password);
  }
}

Si te sale un error diciendo que los inputs no tienen la propiedad ngModel o algo así, recuerda que tienes que importar el modulo de FormsModule en el app.componen.tss, eso lo explico en este artículo: Sistema de vista de Vue, cómo se usan los inputs.

Por último los estilos, aquí puedes crear los que maś te gusten. Yo para este ejemplo he decido poner los estilos dentro del fichero styles.css dentro de app ya que para este ejemplo sencillo no pasa nada por tener todos los estilos de forma global (así se pueden aprovechar en todos los componentes). En páginas más grandes recomiendo que crees un componente de formulario o algo así para poder reutilizar en el componente de login y register y que así puedan compartir estilos.

/* app/styles.css */

* {
  box-sizing: border-box;
}
html,
body {
  background: #ecf0f3;
}
h1 {
  margin: 0;
  padding: 0;
}
.custom-form {
  min-width: 300px;
  max-width: 60%;
  margin: 0px auto;
  background: rgba(255, 255, 255, 0.15);
  padding: 2rem 3rem;
  margin: auto;
  border-radius: 2.5rem;
  background-color: #ecf0f3;
  box-shadow: 13px 13px 20px #cbced1, -13px -13px 20px #ffffff;
  color: black;
  margin-top: 10rem;
}
.custom-form input {
  display: block;
  margin: 2rem 0;
  width: 100%;
  border-radius: 0.5rem;
  padding: 1rem;
  border: none;
  box-shadow: inset -5px -5px 15px rgba(255, 255, 255, 0.8), inset 5px 5px 10px
      rgba(0, 0, 0, 0.1);
  border: 0 none;
  background: #ebf5fc;
}
.custom-form button {
  text-transform: uppercase;
  letter-spacing: 0.15em;
  border: none;
  font-size: 0.875rem;
  color: #ffffff;
  font-weight: bold;
  background-color: #bcd8c1;
  width: 100%;
  display: block;
  padding: 0.875rem 1rem;
  border-radius: 1.5rem;
  box-shadow: 3px 3px 8px #b1b1b1, -3px -3px 8px #ffffff;
  cursor: pointer;
}

El resultado quedaría así:

Si pruebas a escribir en los dos inputs y haces clic en el botón podrás ver en la consola del navegador que se imprimen dos valores, el del input del email y el de la contraseña. Estos dos valores más adelantes son los que pasaremos al servidor.

Vamos con el componente para el resgistro de usuarios.

Este componente es prácticamente igual que el del login solo que cambian un par de textos y que hay un input más para confirmar la contraseña, vamos a ello:

En el register.component.html:

<!-- register.component.html -->

<div class="register">
  <form class="custom-form" method="post">
    <h1>Register</h1>
    <input
      type="email"
      [(ngModel)]="email"
      name="email"
      placeholder="Email"
      required="required"
    />
    <input
      type="password"
      [(ngModel)]="password"
      name="password"
      placeholder="Password"
      required="required"
    />
    <input
      type="password"
      [(ngModel)]="confirmPassword"
      name="password"
      placeholder="Repeat the password"
      required="required"
    />
    <button type="submit" (click)="register()">Register</button>
  </form>
</div>

Y en el register.component.ts:

// register.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-register",
  templateUrl: "./register.component.html",
  styleUrls: ["./register.component.css"]
})
export class RegisterComponent {
  email: string;
  password: string;
  confirmPassword: string;

  constructor() {}

  register() {
    console.log(this.email);
    console.log(this.password);
  }
}

Y quedaría así:

🎉 Pues listo, primera tarea completada, hacemos commit y pasamos a la siguiente tarea.

[X] 🏗️ Crear y maquetar las vistas de login y registro con formularios

Creación del sistema de login en Angular

Para esta segunda tarea tenemos que crear servicios. Si recuerdas el artículo anterior, lo recomendable para leer o modificar datos es crear servicios para tener esa capa separada.

Aquí lo que se suele hacer es crear un servicio por entidad (tabla en la base de datos). Para este ejemplo voy a crear un servicio para leer, crear y modificar usuarios. En este servicio voy a incluir la lógica de hacer login y registro.

Lo que hago es crear una nueva carpeta dentro de src/app llamada users. Dentro de esa carpeta creo un nuevo archivo llamado users.service.ts.

Recuerda importar también HttpClientModule dentro de la sección imoports del app.module.ts:

import { HttpClientModule } from '@angular/common/http';

A continuación creo la siguiente estructura dentro del archivo:

// src/app/users/users.service.ts

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

@Injectable({
  providedIn: "root"
})
export class UsersService {
  constructor(private http: HttpClient) {}

  login(user: Any): Observable<any> {
    return this.http.post("https://reqres.in/api/login", user);
  }
}

Por cierto, que no lo he dicho, al igual que hicimos en el artículo anterior, vamos a usar la API de https://reqres.in. Si tienes tu propia API creada puedes usar la tuya. Recuerda que para hacer una API necesitas un lenguaje de servidor: Python, Java, NodeJS, etc.

Como ves de momento he creado en el servicio un solo método para hacer login que simplemente llama la API haciendo un POST y pasando un objeto user.

Vamos ahora a conectar este servicio con el componente de login.component.ts que creamos antes:

// src/app/login/login.component.ts

import { Component } from "@angular/core";
import { UsersService } from "../users/users.service";

@Component({
  selector: "app-login",
  templateUrl: "./login.component.html",
  styleUrls: ["./login.component.css"]
})
export class LoginComponent {
  email: string;
  password: string;

  constructor(public userService: UsersService) {}

  login() {
    const user = {email: this.email, password: this.password};
    this.userService.login(user).subscribe( data => {
      console.log(data);
    });
  }
}

Listo, si abres la página de login, metes el email eve.holt@reqres.in y la contraseña que quieras verás que se devuelve por consola el token.

En la siguiente sección veremos cómo almacenar ese token JWT para poder enviarlo en las demás peticiones.

Vamos ahora con el registro. Igual que pasaba antes es muy similar al login. El servicio de users quedaría de la siguiente manera:

// src/app/users/users.service.ts

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

@Injectable({
  providedIn: "root"
})
export class UsersService {
  constructor(private http: HttpClient) {}

  login(user: Any): Observable<any> {
    return this.http.post("https://reqres.in/api/login", user);
  }
  register(user: Any): Observable<any> {
    return this.http.post("https://reqres.in/api/register", user);
  }
}

Y el componente register.component.ts así:

// src/app/register/register.component.ts

import { Component } from "@angular/core";
import { UsersService } from "../users/users.service";

@Component({
  selector: "app-register",
  templateUrl: "./register.component.html",
  styleUrls: ["./register.component.css"]
})
export class RegisterComponent {
  email: string;
  password: string;
  password: string;
  passwordError: boolean;

  constructor(public userService: UsersService) {}

  register() {
    const user = { email: this.email, password: this.password };
    this.userService.register(user).subscribe(data => {
      console.log(data);
    });
  }
}

De igual forma, si pruebas en la página de /register a meter el email **eve.holt@reqres.in y la contraeña que quieras, podrás ver en la consola del navegador que se devuelve el usuario con token creado.

Pues otra tarea terminada. Commit y a por la siguiente:

[X] ✨ Conectar el sistema de login y registro con la API

Almacenar el token JWT en las cookies

Para usar las cookies en Angular existe esta librería para que no tegnas que montarte tu todo. Para instalarla:

npm install ngx-cookie-service --save

Para usar este modulo tienes que importarlo en el app.module.ts. Dentro de la sección providers, ya que es un servicio.

// app.module.ts

import { routing } from "./app.routing";
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";
import { LoginComponent } from "./login/login.component";
import { RegisterComponent } from "./register/register.component";
import { HttpClientModule } from "@angular/common/http";
import { CookieService } from 'ngx-cookie-service';

@NgModule({
  declarations: [AppComponent, LoginComponent, RegisterComponent],
  imports: [BrowserModule, routing, FormsModule, HttpClientModule],
  providers: [CookieService],
  bootstrap: [AppComponent]
})
export class AppModule {}

Para aislar también esta lógica de las cookies, vamos a meter dos nuevos métodos en el servicio de usuarios, uno para guardar token en cookies y otro para recuperarlo:

// src/app/users/users.service.ts

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import { CookieService } from "ngx-cookie-service";

@Injectable({
  providedIn: "root"
})
export class UsersService {
  constructor(private http: HttpClient, private cookies: CookieService) {}

  login(user: Any): Observable<any> {
    return this.http.post("https://reqres.in/api/login", user);
  }
  register(user: Any): Observable<any> {
    return this.http.post("https://reqres.in/api/register", user);
  }
  setToken(token: String) {
    this.cookies.set("token", token);
  }
  getToken() {
    return this.cookies.get("token");
  }
}

Ya solo queda llamar desde los componentes de login y register al servicio de usuarios para almacenar el token que llega desde la API

// src/app/login/login.component.ts

...

login() {
  const user = { email: this.email, password: this.password };
  this.userService.login(user).subscribe(data => {
    this.userService.setToken(data.token);
  });
}
// src/app/register/register.component.ts

...

register() {
  const user = { email: this.email, password: this.password };
  this.userService.register(user).subscribe(data => {
    this.userService.setToken(data.token);
  });
}

Si abres la página y intentas hacer login o register y abres las herramientas de desarrollador del navegador (Botón derecho > Inspeccionar elemento). En una de las pestañas podrás ver las cookies que almacena el navegador y una de ellas será la del token.

Por último, como detalle final vamos a hacer que al hacer login o register si la petición ha ido bien que redirige a la página principal. Para ello tienes que importar el router en el componente e inyectarlo en el constructor:

// src/app/login/login.component.ts

import { Component } from "@angular/core";
import { UsersService } from "../users/users.service";
import { Router } from '@angular/router';

@Component({
  selector: "app-login",
  templateUrl: "./login.component.html",
  styleUrls: ["./login.component.css"]
})
export class LoginComponent {
  email: string;
  password: string;

  constructor(public userService: UsersService, public router: Router) {}

  login() {
    const user = { email: this.email, password: this.password };
    this.userService.login(user).subscribe(data => {
      this.userService.setToken(data.token);
      this.router.navigateByUrl('/');
    });
  }
}

Para el componente de register sería igual.

Por cierto si quieres saber cuándo hay errores en la llamada a la API lo pudes hacer así:

login() {
  const user = { email: this.email, password: this.password };
  this.userService.login(user).subscribe(
    data => {
      this.userService.setToken(data.token);
      this.router.navigateByUrl('/');
    },
    error => {
      console.log(error);
    });
}

Mostrar el usuario logueado

Ahora que tenemos el token en las cookies. Lo malo de la API esta de ejemplo que estamos usando es que no tiene ninguna llamada de ejemplo que devuelva la información de usuario si pasamos un token.

Para un ejemplo real lo suyo sería crear un endpoint al que puedas pasar un token y te de el usuario.

Para no dejar el articulo aquí voy a usar la petición a la API de buscar un usuario por su ID, en un ejemplo real sería lo mismo solo que pasando el token.

Voy a crear otro componente llamado Home. Para ello creo la carpeta home y dentro creo los 3 archivos que tienen los componentes: home.component.ts, home.component.html y home.component.css.

Además, lo añadimos al app.module.tss igual que hemos hecho con el del login y el regitser y le asignamos una ruta dentro del app.routing.ts. En mi caso la ruta de /home.

Hecho esto añadimos dentro del servicio de usuarios el nuevo método para recuperar la información de usuario desde la API:

// src/app/users/users.service.ts

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import { CookieService } from "ngx-cookie-service";

@Injectable({
  providedIn: "root"
})
export class UsersService {
  constructor(private http: HttpClient, private cookies: CookieService) {}

  login(user: Any): Observable<any> {
    return this.http.post("https://reqres.in/api/login", user);
  }
  register(user: Any): Observable<any> {
    return this.http.post("https://reqres.in/api/register", user);
  }
  setToken(token: String) {
    this.cookies.set("token", token);
  }
  getToken() {
    return this.cookies.get("token");
  }
  getUser() {
    return this.http.get("https://reqres.in/api/users/2");
  }
  getUserLogged() {
    const token = this.getToken();
    // Aquí iría el endpoint para devolver el usuario para un token
  }
}

El componente de Home quedaría así:

// src/app/home/home.component.ts

import { Component, OnInit } from "@angular/core";
import { UsersService } from "../users/users.service";

@Component({
  selector: "app-home",
  templateUrl: "./home.component.html",
  styleUrls: ["./home.component.css"]
})
export class HomeComponent implements OnInit {
  constructor(public userService: UsersService) {}
  ngOnInit() {
    this.getUserLogged();
  }
  getUserLogged() {
    this.userService.getUser().subscribe(user => {
      console.log(user);
    });
  }
}

Para este ejemplo siempre se devuelve el mismo usaurio porque siempre se llama al mismo endpoint. Aquí es donde digo que en un caso real, dentro del componente desde el que quieras ver el usuario logueado, tienes que llamar al userService para que te devuelva el token desde las cookkies para pasarlo al método de getUser del servicio, pero como digo con la API esta de ejemplo no se puede.

En el momento en el que quieras hacer logout simplemente tienes que borrar la cookie haciendo:

this.cookies.delete("token");

Pues la última tarea terminada también. Commit y push.

La página principal la he dejado sin estilos, pero puedes aprovechar para cambiarlos.

Te dejo el proyecto subido a Codesanbox para que puedas jugar con él:

🖥️ Proyecto completo en codesanbox

Para terminar te pongo una pequeña lista de tareas como deberes:

  • [ ] Mostrar un mensaje de error dentro de los formularios de login y registro cuando hay un error en la petición
  • [ ] Comprobar que las dos contraseñas coinciden al pulsar sobre el botón de registro y mostrar un mensaje en caso de que no coincidan
  • [ ] Crear un componente de navbar. Dentro mostrar el usuario logueado y en caso de no existir mostrar el botón de login
  • [ ] Crear la funcionalidad de logout borrando la cookie. Dentro del navbar mostrar el botón de log out en caso de que la sesión no este iniciada

Conclusiones

Siento si este artículo ha sido demasiado largo, pero quería explicarlo todo como si fuera un ejemplo real, espero que te haya servido de ayuda para crear tu propio sistema de login para Angular.

En un ejemplo real también faltaría por insertar el token en todas las llamadas al backend que se hagan.

Con lo que hemos visto hasta ahora ya deberías ser capaz de crear aplicaciones web sencillas con Angular. En próximos articulos pasaremos a ver conceptos algo más avanzados que facilitan las cosas pero no añaden nada que no puedas hacer ya.