18

May

Publicado por: Xavi Cobos | Angular | Angular2 | app development | desarrollo | development | web development

Cómo crear Login y autenticación con Angular

A continuación os mostraré cómo crear una página de login y los diferentes componentes implicados en la autenticación de un usuario mediante el framework Angular.

El proyecto lo podréis encontrar en mi repositorio de GitHub: https://github.com/xavics/angular-login

Requisitos previos

Antes de empezar deberíais saber que este proyecto está creado mediante el cliente Angular cli. Para los que no lo conozcan, podéis encontrar toda la información en el siguiente link: https://github.com/angular/angular-cli.

Además, este contiene unas dependencias que piden como mínimo Node >= 6.9.0 y NPM >= 3. Para cumplir con estos requisitos lo único que tenemos que hacer es instalar Node, lo podréis encontrar en la web oficial (https://nodejs.org/en/download/)

Probamos el proyecto…

Para probar el proyecto debéis seguir estos pasos:

  1. Asegurarnos que cumplimos los requisitos mencionados previamente.
  2. Clonar el repositorio: https://github.com/xavics/angular-login.
  3. Instalar las dependencias y lanzar la aplicación de forma local. Para hacerlo, debemos ejecutar en la raíz del proyecto:
$ npm install && npm start

¡Ya podemos empezar a programar!

Una vez hecha la prueba del proyecto, os explicaré la función de las diferentes partes del proyecto: modelos, servicios, backend, componentes, modulos, guards. Pero antes, os mostraré el árbol de directorios para tener una idea general del proceso.

Arbol de directorios

El directorio donde se almacena el código creado se guarda en ./src/app y tiene la estructura que vemos a continuación. Este no servirá para situarnos durante el proceso de creación del login. Para estructurar el proyecto seguiremos las guías de estilo de Angular2, siempre usando nuestras preferencias particulares para trabajar de forma más eficiente.

./src/app
├── core
│   ├── guards
│   │   └── authorizated.guard.ts
│   ├── helper
│   │   └── fake-backend.ts
│   ├── mocks
│   │   └── mock-users.ts
│   ├── models
│   │   ├── session.model.ts
│   │   └── user.model.ts
│   ├── services
│   │   └── storage.service.ts
│   └── core.module.ts
├── home
│   ├── home.component.html
│   └── home.component.ts
├── login
│   ├── shared
│   │   ├── authentication.service.ts
│   │   └── login-object.model.ts
│   ├── login.component.html
│   └── login.component.ts
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
└── app.routing.ts
Models

Los modelos nos permitiren obtener los objetos necesarios para este login. En principio, con un modelo de Usuario sería suficiente, pero también cremoa un Session y LoginObject.

User

Definiremos las propiedades mínimas requeridas que debe tener un usuario.

export class User {
    public id: number;
    public name: string;
    public surname: string;
    public email: string;
    public username: string;
    public password?: string;
}
Session

Este modelo representa una sesión activa. Esta la utilizaremos para guardar el usuario, una vez logueado correctamente, y el token de autentificación que necesitaremos en un futuro para hacer peticiones al backend.

export class Session {
  public token: string;
  public user: User;
}
LoginObject

Es una objeto que hemos typeado para tener más controlada la información que se enviará a la petición de login.

export class LoginObject {

  public username: string;
  public password: string;

  constructor( object: any){
    this.username = (object.username) ? object.username : null;
    this.password = (object.password) ? object.password : null;
  }
}
Services
Authentication

Este servicio permite comunicarnos con el servidor para hacer login a través de una petición HTTP (Post) enviando un nombre de usuario y una contraseña. Se tiene que pasar por constructor el Http de Angular para ejecutar las peticiones. La petición de login nos devolverá un observable de tipo Session que almacenaremos después. El logout nos devolverá un observable de tipo Boolean.

import {Injectable} from "@angular/core";
import {Http, Response} from "@angular/http";
import {Observable} from "rxjs";
import {LoginObject} from "./login-object.model";
import {Session} from "../../core/models/session.model";

@Injectable()
export class AuthenticationService {

 constructor(private http: Http) {}

 private basePath = '/api/authenticate/';

 login(loginObj: LoginObject): Observable<Session> {
 return this.http.post(this.basePath + 'login', loginObj).map(this.extractData);
 }

 logout(): Observable<Boolean> {
 return this.http.post(this.basePath + 'logout', {}).map(this.extractData);
 }

 private extractData(res: Response) {
 let body = res.json();
 return body;
 }
}
Storage

Servicio auxiliar para administrar el token y usuario almacenados cuando se hace un login. Este servicio permite utilizar la información del usuario que se ha logueado desde cualquier lugar. También tenemos un método para eliminar la información almacenada y posteriormente regresar a la pantalla de login.

import {Injectable} from "@angular/core";
import { Router } from '@angular/router';
import {Session} from "../models/session.model";
import {User} from "../models/user.model";

@Injectable()
export class StorageService {

  private localStorageService;
  private currentSession : Session = null;

  constructor(private router: Router) {
    this.localStorageService = localStorage;
    this.currentSession = this.loadSessionData();
  }

  setCurrentSession(session: Session): void {
    this.currentSession = session;
    this.localStorageService.setItem('currentUser', JSON.stringify(session));
  }

  loadSessionData(): Session{
    var sessionStr = this.localStorageService.getItem('currentUser');
    return (sessionStr) ? <Session> JSON.parse(sessionStr) : null;
  }

  getCurrentSession(): Session {
    return this.currentSession;
  }

  removeCurrentSession(): void {
    this.localStorageService.removeItem('currentUser');
    this.currentSession = null;
  }

  getCurrentUser(): User {
    var session: Session = this.getCurrentSession();
    return (session && session.user) ? session.user : null;
  };

  isAuthenticated(): boolean {
    return (this.getCurrentToken() != null) ? true : false;
  };

  getCurrentToken(): string {
    var session = this.getCurrentSession();
    return (session && session.token) ? session.token : null;
  };

  logout(): void{
    this.removeCurrentSession();
    this.router.navigate(['/login']);
  }

}
 Backend

Implementamos un login sin backend que lo respalde, por lo tanto hemos creado una versión muy simplificada de un fake backend con los endpoints que necesitamos para hacerlo funcionar.

La función login nos devuelve un token y el usuario en el body que coincidirá con los parámetros que reciba. Si los parámetros no son correctos devolverá un error. El logout simplemente nos devuelve un true.

import {Http, BaseRequestOptions, Response, ResponseOptions, RequestMethod, ResponseType} from '@angular/http';
import { MockBackend, MockConnection } from '@angular/http/testing';
import {User} from "../models/user.model";
import {USERS} from "../mocks/mock-users";

class MockError extends Response implements Error {
  name:any;
  message:any;
}

export function fakeBackendFactory (backend: MockBackend, options: BaseRequestOptions) {
  backend.connections.subscribe((connection: MockConnection) => {
    setTimeout(() => {
      // fake authenticate api end point
      if (connection.request.url.endsWith('/api/authenticate/login') && connection.request.method === RequestMethod.Post) {
        let params = JSON.parse(connection.request.getBody());

        // check user credentials and return fake jwt token if valid
        let found: User = USERS.find((user: User) => {return (params.username === user.username);});
        if (found) {
          if(params.password === found.password) {
            connection.mockRespond(new Response(
              new ResponseOptions({status: 200, body: {token: 'fake-token-jwt', user: found}})
            ));
          }else{
            connection.mockError(new MockError(new ResponseOptions({type:ResponseType.Error, status:400, body: JSON.stringify({code: 2, message: 'The password does not match '})})));
          }
        } else {
          connection.mockError(new MockError(new ResponseOptions({type:ResponseType.Error, status:400, body: JSON.stringify({code: 1, message: 'Username does not exists'})})));
        }

      }

      if (connection.request.url.endsWith('/api/authenticate/logout') && connection.request.method === RequestMethod.Post) {
        let params = JSON.parse(connection.request.getBody());
        connection.mockRespond(new Response(
          new ResponseOptions({status: 200, body: true})
        ));
      }
    }, 100);

  });

  return new Http(backend, options);
}

export let fakeBackendProvider = {
  // use fake backend in place of Http service for backend-less development
  provide: Http,
  useFactory: fakeBackendFactory,
  deps: [MockBackend, BaseRequestOptions]
};
Componentes

Los componentes normalmente están compuestos por «ts|spec.ts|scss|html». No se ha puesto ni test ni diseño, por lo que solo hemos realizado un template y un componente (entendiéndolo como el .ts) para cada uno de ellos.

Login

Por un lado tenemos el template con el formulario, es un formulario simple que utiliza los reactive forms. Por otro lado, integramos diferentes validaciones de los campos.

<div class="aligner">
  <md-card class="aligner-center-item">
    <md-card-title>
      Login Form
    </md-card-title>
    <md-card-content>
      <form id="login-form" #lForm="ngForm" [formGroup]="loginForm" (ngSubmit)="submitLogin()" novalidate>
        <md-input-container style="width: 100%; margin-bottom: 10px;" [ngClass]="{'mat-input-invalid': submitted && error?.code == 1}">
          <input mdInput type="text" formControlName="username" placeholder="Username"/>
          <md-hint style="color: red;" *ngIf="submitted && error?.code == 1 && !loginForm.controls.username.hasError('required')">{{error.message}}</md-hint>
          <md-hint style="color: red;" *ngIf="submitted && loginForm.controls.username.hasError('required')">Field is mandatory</md-hint>
        </md-input-container>
        <md-input-container style="width: 100%;" [ngClass]="{'mat-input-invalid': submitted && error?.code == 2}">
          <input mdInput type="password" formControlName="password" placeholder="Password"/>
          <md-hint style="color: red;" *ngIf="submitted && error?.code == 2 && !loginForm.controls.password.hasError('required')">{{error.message}}</md-hint>
          <md-hint style="color: red;" *ngIf="submitted && loginForm.controls.password.hasError('required')">Field is mandatory</md-hint>
        </md-input-container>
      </form>
    </md-card-content>
    <md-card-actions style="text-align: right;">
      <button md-raised-button type="submit" form="login-form"> Sign In</button>
    </md-card-actions>
  </md-card>
</div>

En el .ts definimos los campos del formulario mediante el formbuilder , además de la validación del formulario cuando se hace el submit y enviar la petición de login.

import {Component} from "@angular/core";
import {Validators, FormGroup, FormBuilder} from "@angular/forms";
import {LoginObject} from "./shared/login-object.model";
import {AuthenticationService} from "./shared/authentication.service";
import {StorageService} from "../core/services/storage.service";
import {Router} from "@angular/router";
import {Session} from "../core/models/session.model";
@Component({
  selector: 'login',
  templateUrl: 'login.component.html'
})

export class LoginComponent {
  public loginForm: FormGroup;
  public submitted: Boolean = false;
  public error: {code: number, message: string} = null;

  constructor(private formBuilder: FormBuilder,
              private authenticationService: AuthenticationService,
              private storageService: StorageService,
              private router: Router) { }

  ngOnInit() {
    this.loginForm = this.formBuilder.group({
      username: ['', Validators.required],
      password: ['', Validators.required],
    })
  }

  public submitLogin(): void {
    this.submitted = true;
    this.error = null;
    if(this.loginForm.valid){
      this.authenticationService.login(new LoginObject(this.loginForm.value)).subscribe(
        data => this.correctLogin(data),
        error => this.error = JSON.parse(error._body)
      )
    }
  }

  private correctLogin(data: Session){
    this.storageService.setCurrentSession(data);
    this.router.navigate(['/home']);
  }
}
 Home

Creamos un componente «home» para comprobar la funcionalidad de la autenticación y así demostrar cómo utilizar el StorageService para obtener el usuario. También podemos hacer un logout.

<h1>Hi {{user.name}}</h1>
<button md-raised-button type="button" (click)="logout()">Log Out</button>
import {Component} from "@angular/core";
import {StorageService} from "../core/services/storage.service";
import {User} from "../core/models/user.model";
import {AuthenticationService} from "../login/shared/authentication.service";
@Component({
  selector: 'home',
  templateUrl: 'home.component.html'
})

export class HomeComponent {
  public user: User;

  constructor(
    private storageService: StorageService,
    private authenticationService: AuthenticationService
  ) { }

  ngOnInit() {
    this.user = this.storageService.getCurrentUser();
  }

  public logout(): void{
    this.authenticationService.logout().subscribe(
        response => {if(response) {this.storageService.logout();}}
    );
  }

}
 App

El componente App es la raíz de toda la aplicación. Al ser el componente base, ponemos el router, es decir, el espacio donde los diferentes componentes irán apareciendo según la ruta.

<router-outlet></router-outlet>
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [ AuthenticationService ]
})
export class AppComponent {
}
Guard

Es la clase básica que nos limita el acceso a un componente mediante una ruta. Para ello debemos comprobar que el usuario se esté logueado al sistema utilizando el StorageService, si eso es correcto, permitimos el acceso al componente, en caso negativo se le redirecciona al Login.

import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import {StorageService} from "../services/storage.service";

@Injectable()
export class AuthorizatedGuard implements CanActivate {

  constructor(private router: Router,
              private storageService: StorageService) { }

  canActivate() {
    console.log(this.storageService.isAuthenticated());
    if (this.storageService.isAuthenticated()) {
      // logged in so return true
      return true;
    }

    // not logged in so redirect to login page
    this.router.navigate(['/login']);
    return false;
  }
}
Router

En el router definimos las diferentes rutas de la aplicación, estas están asociadas a un componente. Tenemos dos rutas que nos dirigen a la /home en caso de que la ruta que se ha buscado esté vacía o no definida. El acceso a home está protegido mediante la Guard que comentábamos antes. Por lo tanto, si ponemos una ruta desconocida y no estamos logueados nos redirigirá a la /login.

import { RouterModule, Routes } from '@angular/router';
import {HomeComponent} from "./home/home.component";
import {LoginComponent} from "./login/login.component";
import {AuthorizatedGuard} from "./core/guards/authorizated.guard";

const appRoutes: Routes = [
  { path: 'home', component: HomeComponent, canActivate: [ AuthorizatedGuard ] },
  { path: 'login', component: LoginComponent },
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: '**', redirectTo: '/home'}
];

export const Routing = RouterModule.forRoot(appRoutes);
Modules
 App

Es el módulo principal de la aplicación. En este módulo incluimos todo lo necesario para que la aplicación funcione correctamente. También añadimos el Core module que creamos para fragmentar la aplicación.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {CoreModule} from "./core/core.module";
import {Routing} from "./app.routing";
import {MdInputModule} from '@angular/material';
import {MdButtonModule} from '@angular/material';
import {MdCardModule} from '@angular/material';
import {HomeComponent} from "./home/home.component";
import {LoginComponent} from "./login/login.component";

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    Routing,
    BrowserAnimationsModule,
    CoreModule,
    ReactiveFormsModule,
    MdInputModule,
    MdButtonModule,
    MdCardModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
Core

Es el módulo de la aplicación con servicios que requieren un solo inicio.

import {NgModule, Optional, SkipSelf} from '@angular/core';
import {fakeBackendProvider} from "./helper/fake-backend";
import {MockBackend} from "@angular/http/testing";
import {BaseRequestOptions} from "@angular/http";
import {StorageService} from "./services/storage.service";
import {AuthorizatedGuard} from "./guards/authorizated.guard";

@NgModule({
  declarations: [  ],
  imports: [],
  providers: [
    StorageService,
    AuthorizatedGuard,
    fakeBackendProvider,
    MockBackend,
    BaseRequestOptions
  ],
  bootstrap: []
})
export class CoreModule {
  constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }
}

 

Espero te haya sido de utilidad y sirva como punto de partida para futuros proyectos más complejos. Por ello, si quieres continuar adentrándote en el uso de Angular, puedes continuar con otro tutorial sobre «Cómo transformar tu aplicación Angular en una Progressive Web App«. En este, te muestro el proceso para convertir el trabajo realizado en este artículo (u otro que tu hayas realizado con Angular) en una PWA.

CTA-Join-justdigital

 

Barcelona
Passeig Gaiolà 13
+34 933 801 144
Lleida
Carrer Agustins 7
+34 973 988 222
Andorra
(Escaldes-Engordany)
Parc de la Mola 10, AD700
Bogota
Carrera 9A #99-07 Piso 9. Despacho 02
Torre la Equidad