Publicada:

Angular 14 - Tutorial de autenticación de Facebook con ejemplo

Tutorial construido con Angular 14.2.12

Otras versiones disponibles:

En este tutorial, cubriremos cómo implementar la autenticación de Facebook en Angular 14.

Aplicación de ejemplo de Angular 14

La aplicación Angular de ejemplo contiene las siguientes tres páginas para demostrar cómo iniciar sesión con Facebook, luego ver y editar los detalles de su cuenta:

  • Iniciar sesión (/login) - contiene un botón de inicio de sesión de Facebook que activa el flujo de autenticación con Facebook; en caso de éxito, se devuelve un token de acceso de Facebook a la aplicación Angular que luego se usa para autenticarse con el backend API. La primera vez que inicia sesión, se crea una nueva cuenta para usted en la API de backend.
  • Inicio (/) - muestra los detalles de su cuenta obtenidos de un punto final seguro en la API de backend.
  • Editar cuenta (/edit-account) - contiene un formulario para actualizar los detalles de su cuenta o eliminar su cuenta de la API de backend.

Angular + API + Facebook Login

Estas son las tres piezas involucradas en hacer que el ejemplo funcione:

  • Aplicación Angular - la aplicación frontal (cliente) con páginas para iniciar sesión en Facebook, ver la cuenta y editar la cuenta.
  • API de back-end - la API del servidor que administra y brinda acceso seguro a los datos para la aplicación de front-end. Se implementa mediante una API de back-end falsa en el ejemplo.
  • Facebook - en este escenario, Facebook actúa como un servidor de autenticación (proveedor de identidad) para verificar las credenciales del usuario y devolver un token de acceso en caso de éxito. El token de acceso de FB se usa para autenticarse con la API de backend, que valida el token de acceso con Facebook y devuelve su propio token JWT para acceder a rutas seguras en la API de backend.

Comunicación con Facebook

La comunicación con Facebook en el ejemplo está habilitada con una aplicación de Facebook que creé en el Portal para desarrolladores de Facebook (https://developers.facebook.com/) denominado "JasonWatmore.com Login Example". Las aplicaciones de Facebook se utilizan para configurar qué productos desea aprovechar de Facebook (por ejemplo, inicio de sesión de Facebook). El ID de la aplicación se pasa al SDK de Facebook durante la inicialización (FB.init(...)).

Para implementar el inicio de sesión con Facebook en su propia aplicación Angular (u otra), deberá crear su propia aplicación de Facebook en https://developers.facebook.com/ y configurarlo con el producto Facebook Login. Para obtener más información, consulte los documentos de inicio de sesión de Facebook en https://developers.facebook.com/docs/facebook-login/web.

Flujo de autenticación con Angular, API e inicio de sesión de Facebook (Facebook Login)

Estos son los pasos que suceden cuando un usuario inicia sesión en la aplicación Angular con Facebook:

  1. El usuario hace clic en "Login with Facebook" e ingresa sus credenciales
  2. Facebook verifica las credenciales y devuelve un token de acceso de FB a la aplicación Angular
  3. La aplicación Angular envía el token de acceso a la API de backend para autenticarse
  4. La API backend valida el token de acceso con la API de Facebook
  5. En el primer inicio de sesión, la API backend crea una nueva cuenta para el usuario (con su nombre obtenido de la API de Facebook)
  6. La API backend devuelve un nuevo token JWT de corta duración (15 minutos) a la aplicación Angular
  7. Angular App almacena el JWT y lo usa para realizar solicitudes seguras a la API de backend (¡sesión iniciado!)
  8. La aplicación Angular inicia un temporizador para repetir los pasos 3 a 8 antes de que caduque el JWT para actualizar automáticamente el JWT y mantener la sesión del usuario

API de back-end falsa

La aplicación Angular de ejemplo se ejecuta con una API de back-end falsa de manera predeterminada para permitir que se ejecute completamente en el navegador sin una API real, la API falsa contiene rutas para la autenticación y las operaciones CRUD de la cuenta y utiliza el almacenamiento local del navegador para guardar datos. Para deshabilitar el backend falso, solo tiene que eliminar un par de líneas de código del módulo de la aplicación, puede consultar el fake-backend para ver qué se requiere para construir una API real para el ejemplo.

Diseñado con Bootstrap 5

La aplicación de inicio de sesión de ejemplo está diseñada con CSS de Bootstrap 5.2; para obtener más información sobre Bootstrap, consulte https://getbootstrap.com/docs/5.2/getting-started/introduction/.

Código en GitHub

El proyecto de ejemplo está disponible en GitHub en https://github.com/cornflourblue/angular-14-facebook-login-example.

Aquí está en acción: (Ver en StackBlitz en https://stackblitz.com/edit/angular-14-facebook-login-example)


Ejecute la aplicación de inicio de sesión de Angular Facebook localmente

  1. Instalar Node.js y npm desde https://nodejs.org
  2. Descargue o clone el código fuente del proyecto desde https://github.com/cornflourblue/angular-14-facebook-login-example
  3. Instale todos los paquetes npm necesarios ejecutando npm install desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el package.json).
  4. Inicie la aplicación en modo SSL (https) ejecutando npm run start:ssl desde la línea de comando en la carpeta raíz del proyecto, Se requiere SSL para que el SDK de Facebook funcione correctamente, esto abrirá un navegador con la URL https://localhost:4200/.
  5. Debería ver el mensaje Tu conexión no es privada (o algo similar en los navegadores que no son Chrome), no te preocupes, es solo porque el servidor de desarrollo de Angular se ejecuta con un Certificado SSL autofirmado. Para abrir la aplicación, haga clic en el botón Avanzado y en el enlace Ir a localhost.


Estructura de proyecto angular

La CLI de Angular se usó para generar la estructura del proyecto base con el comando ng new <nombre del proyecto>, la CLI también se usa para construir y servir la aplicación. Para obtener más información sobre la CLI angular, consulte https://angular.io/cli.

La aplicación y la estructura del código del tutorial siguen principalmente las recomendaciones de mejores prácticas de la guía oficial de estilo angular, con algunos de mis propios ajustes aquí y allá.

Estructura de carpetas

Cada función tiene su propia carpeta (home e login), otros códigos compartidos/comunes, como services, models, helpers, etc., se colocan en carpetas con el prefijo _ para diferenciarlas fácilmente de las funciones y agruparlas en la parte superior de la estructura de carpetas.

Archivos de barril

Los archivos index.ts en cada carpeta son archivos de barril que agrupan los módulos exportados de esa carpeta para que puedan importarse usando solo la ruta de la carpeta en lugar de la ruta completa del módulo, y para habilitar importar varios módulos en una sola importación (por ejemplo, import { HomeComponent, EditAccountComponent } from './home').

Alias de ruta de TypeScript

Los alias de ruta @app y @environments se han configurado en tsconfig.json que se asignan a la directorios /src/app y /src/environments. Esto permite que las importaciones sean relativas a las carpetas de aplicaciones y entornos al anteponer rutas de importación con alias en lugar de tener que usar rutas relativas largas (por ejemplo, import MyComponent from '../../../MyComponent').

Aquí están los principales archivos del proyecto que contienen la lógica de la aplicación, dejé fuera algunos archivos que fueron generados por el comando Angular CLI ng new que yo no cambié.

 

Inicializador de aplicaciones

Ruta: /src/app/_helpers/app.initializer.ts

El inicializador de la aplicación se ejecuta antes de que se inicie la aplicación Angular para cargar e inicializar el SDK de Facebook y obtener el estado de inicio de sesión del usuario de Facebook. Si el usuario ya ha iniciado sesión con Facebook, se iniciará automáticamente en la API de back-end (y la aplicación Angular) utilizando el token de acceso de Facebook, el usuario es luego redirigido a la página de inicio por el componente de inicio de sesión.

El inicializador de la aplicación se agrega a la aplicación angular en la sección providers del módulo de la aplicación utilizando el token de inyección APP_INITIALIZER. Para obtener más información, consulte Angular - Execute an init function before app startup.

import { finalize } from 'rxjs';

import { AccountService } from '@app/_services';
import { environment } from '@environments/environment';

export function appInitializer(accountService: AccountService) {
    return () => new Promise(resolve => {
        // wait for facebook sdk to initialize before starting the angular app
        window.fbAsyncInit = function () {
            FB.init({
                appId: environment.facebookAppId,
                cookie: true,
                xfbml: true,
                version: 'v8.0'
            });

            // auto login to the api if already logged in with facebook
            FB.getLoginStatus(({ authResponse }) => {
                if (authResponse) {
                    accountService.loginApi(authResponse.accessToken)
                        .pipe(finalize(() => resolve(null)))
                        .subscribe();
                } else {
                    resolve(null);
                }
            });
        };

        // load facebook sdk script
        (function (d, s, id) {
            var js: any;
            var fjs: any = d.getElementsByTagName(s)[0];
            if (d.getElementById(id)) { return; }
            js = d.createElement(s); 
            js.id = id;
            js.src = "https://connect.facebook.net/en_US/sdk.js";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));
    });
}
 

Guardia de autenticación

Ruta: /src/app/_helpers/auth.guard.ts

Auth guard es un protector de ruta angular que se utiliza para evitar que los usuarios no autorizados accedan a rutas restringidas. Lo hace mediante la implementación de la interfaz CanActivate que permite que el protector decida si una ruta se puede activar con el método canActivate(). Si el método devuelve true, la ruta está activada (permitida continuar); de lo contrario, si el método devuelve false, la ruta está bloqueada.

La protección de autenticación utiliza el servicio de cuenta para verificar si el usuario ha iniciado sesión, si el usuario no ha iniciado sesión, es necesario. redirigido a la página /login con returnUrl en los parámetros de consulta.

Los protectores de ruta angular se adjuntan a las rutas en la configuración del enrutador, este protector de autenticación se usa en app-routing.module.ts para proteger las rutas "inicio" y "editar de cuenta".

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AccountService } from '@app/_services';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private accountService: AccountService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const account = this.accountService.accountValue;
        if (account) {
            // logged in so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
        return false;
    }
}
 

Interceptor de errores

Ruta: /src/app/_helpers/error.interceptor.ts

El interceptor de errores intercepta las respuestas http de la API para verificar si hubo algún error. Si hay una respuesta 401 No autorizado o 403 Prohibido, la cuenta se desconecta automáticamente de la aplicación, todos los demás errores se vuelven a generar al servicio o componente que llama para ser manejado.

Se implementa usando la interfaz Angular HttpInterceptor incluida en HttpClientModule, al implementar la interfaz HttpInterceptor puede crear un interceptor personalizado para capturar todas las respuestas de error de la API en una sola ubicación.

Los interceptores HTTP se agregan a la canalización de solicitudes en la sección de proveedores del archivo app.module.ts.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AccountService } from '@app/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if ([401, 403].includes(err.status)) {
                // auto logout if 401 or 403 response returned from api
                this.accountService.logout();
            }

            const error = err.error?.message || err.statusText;
            console.error(err);
            return throwError(() => error);
        }))
    }
}
 

API de back-end falso

Ruta: /src/app/_helpers/fake-backend.ts

Para ejecutar y probar la aplicación Angular sin una API de back-end real, el ejemplo usa un backend falso que intercepta las solicitudes HTTP de la aplicación Angular y envía respuestas "falsas". Esto lo hace una clase que implementa la interfaz HttpInterceptor de Angular; para obtener más información sobre los interceptores HTTP de Angular, consulte https://angular.io/api/common/http/HttpInterceptor o este artículo.

El backend falso está organizado en una función handleRoute() de nivel superior que verifica la URL y el método de la solicitud para determinar cómo se debe manejar la solicitud. Para las rutas interceptadas, se llama a una de las siguientes // funciones de ruta, para todas las demás rutas, la solicitud se pasa al backend real llamando a next.handle(request). Debajo de las funciones de ruta hay // funciones auxiliares para devolver diferentes tipos de respuesta y realizar tareas pequeñas.

Para obtener más información, consulte Angular 14 - API de backend falsa para interceptar solicitudes HTTP en desarrollo.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError, from } from 'rxjs';
import { delay, materialize, dematerialize, concatMap } from 'rxjs/operators';

// array in local storage for accounts
const accountsKey = 'angular-14-facebook-login-accounts';
let accounts: any[] = JSON.parse(localStorage.getItem(accountsKey)!) || [];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        // wrap in delayed observable to simulate server api call
        return handleRoute();

        function handleRoute() {
            switch (true) {
                case url.endsWith('/accounts/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/accounts/current') && method === 'GET':
                    return getAccount();
                case url.endsWith('/accounts/current') && method === 'PUT':
                    return updateAccount();
                case url.endsWith('/accounts/current') && method === 'DELETE':
                    return deleteAccount();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }
        }

        // route functions

        function authenticate() {
            const { accessToken } = body;

            return from(new Promise(resolve => {
                fetch(`https://graph.facebook.com/v8.0/me?access_token=${accessToken}`)
                    .then(response => resolve(response.json()));
            })).pipe(concatMap((data: any) => {
                if (data.error) return unauthorized(data.error.message);

                let account = accounts.find(x => x.facebookId === data.id);
                if (!account) {
                    // create new account if first time logging in
                    account = {
                        id: newAccountId(),
                        facebookId: data.id,
                        name: data.name,
                        extraInfo: `This is some extra info about ${data.name} that is saved in the API`
                    }
                    accounts.push(account);
                    localStorage.setItem(accountsKey, JSON.stringify(accounts));
                }

                return ok({
                    ...account,
                    token: generateJwtToken(account)
                });
            }));
        }

        function getAccount() {
            if (!isLoggedIn()) return unauthorized();
            return ok(currentAccount());
        }

        function updateAccount() {
            if (!isLoggedIn()) return unauthorized();

            let params = body;
            let account = currentAccount();

            // update and save account
            Object.assign(account, params);
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok(account);
        }

        function deleteAccount() {
            if (!isLoggedIn()) return unauthorized();

            // delete account then save
            accounts = accounts.filter(x => x.id !== currentAccount().id);
            localStorage.setItem(accountsKey, JSON.stringify(accounts));
            return ok();
        }
        
        // helper functions

        function ok(body?: any) {
            return of(new HttpResponse({ status: 200, body }))
                .pipe(delay(500));
        }

        function unauthorized(message = 'Unauthorized') {
            return throwError(() => ({ status: 401, error: { message } }))
                .pipe(materialize(), delay(500), dematerialize());
        }

        function isLoggedIn() {
            return headers.get('Authorization')?.startsWith('Bearer fake-jwt-token');
        }

        function newAccountId() {
            return accounts.length ? Math.max(...accounts.map(x => x.id)) + 1 : 1;
        }

        function currentAccount() {
            // check if jwt token is in auth header
            const authHeader = headers.get('Authorization');
            if (!authHeader?.startsWith('Bearer fake-jwt-token')) return;

            // check if token is expired
            const jwtToken = JSON.parse(atob(authHeader.split('.')[1]));
            const tokenExpired = Date.now() > (jwtToken.exp * 1000);
            if (tokenExpired) return;

            const account = accounts.find(x => x.id === jwtToken.id);
            return account;
        }

        function generateJwtToken(account: any) {
            // create token that expires in 15 minutes
            const tokenPayload = { 
                exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000),
                id: account.id
            }
            return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`;
        }
    }
}

export let fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};
 

Interceptor JWT

Ruta: /src/app/_helpers/jwt.interceptor.ts

El interceptor JWT intercepta las solicitudes http de la aplicación para agregar un token de autenticación JWT al encabezado de autorización si el usuario inició sesión y la solicitud es a la URL de API de la aplicación Angular (environment.apiUrl).

Se implementa usando la interfaz HttpInterceptor incluida en HttpClientModule, al implementar la interfaz HttpInterceptor, puede crear un interceptor personalizado para modificar las solicitudes http antes de que se envíen al servidor.

Los interceptores HTTP se agregan a la canalización de solicitudes en la sección de proveedores del archivo app.module.ts.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { environment } from '@environments/environment';
import { AccountService } from '@app/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add auth header with jwt if account is logged in and request is to the api url
        const account = this.accountService.accountValue;
        const isLoggedIn = account?.token;
        const isApiUrl = request.url.startsWith(environment.apiUrl);
        if (isLoggedIn && isApiUrl) {
            request = request.clone({
                setHeaders: { Authorization: `Bearer ${account.token}` }
            });
        }

        return next.handle(request);
    }
}
 

Modelo de cuenta

Ruta: /src/app/_models/account.ts

El modelo de cuenta es una pequeña interfaz que define las propiedades de una cuenta.

export interface Account {
    id: string;
    facebookId: string;
    name: string;
    extraInfo: string;
    token?: string;
}
 

Servicio de cuenta

Ruta: /src/app/_services/account.service.ts

El servicio de cuentas maneja la comunicación entre la aplicación Angular y la API de back-end para todo lo relacionado con las cuentas. Contiene métodos para iniciar y cerrar sesión, así como métodos CRUD estándar para recuperar y modificar datos de cuenta.

El método login() primero llama al método loginFacebook() para iniciar sesión en Facebook, luego canaliza el token de acceso de Facebook al método loginApi() para iniciar sesión en la API de backend.

Al iniciar sesión con éxito, la API devuelve los detalles de la cuenta y un token JWT, el método loginApi() luego publica los datos para todos los suscriptores con una llamada a this.accountSubject.next(account). Luego se inicia un temporizador para volver a autenticarse automáticamente en segundo plano para mantener al usuario conectado (this.startAuthenticateTimer()).

El método logout() cierra la sesión de Facebook llamando al método SDK FB.logout(), cancela el temporizador de reautenticación que se ejecuta en segundo plano llamando this.stopAuthenticateTimer(), cierra la sesión del usuario de la aplicación Angular publicando null para todos los suscriptores (this.accountSubject.next(null) ), y redirige a la página de inicio de sesión.

La propiedad account expone un RxJS observable (Observable<Account>) para que cualquier componente pueda suscribirse y recibir una notificación cuando un usuario inicie sesión, cierre sesión o actualice su cuenta. . La notificación se activa con la llamada a this.accountSubject.next() desde cada uno de los métodos correspondientes del servicio. Para obtener más información sobre la comunicación de componentes con RxJS, consulte Angular 14 - Comunicación entre componentes con RxJS Observable y Subject.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, from, of, EMPTY } from 'rxjs';
import { map, concatMap, finalize } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { Account } from '@app/_models';

const baseUrl = `${environment.apiUrl}/accounts`;

@Injectable({ providedIn: 'root' })
export class AccountService {
    private accountSubject: BehaviorSubject<Account | null>;
    public account: Observable<Account | null>;

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.accountSubject = new BehaviorSubject<Account | null>(null);
        this.account = this.accountSubject.asObservable();
    }

    public get accountValue() {
        return this.accountSubject.value;
    }

    login() {
        // login with facebook then the API to get a JWT auth token
        return this.loginFacebook().pipe(
            concatMap(accessToken => this.loginApi(accessToken))
        );
    }

    loginFacebook() {
        // login with facebook and return observable with fb access token on success
        const fbLoginPromise = new Promise<fb.StatusResponse>(resolve => FB.login(resolve));
        return from(fbLoginPromise).pipe(
            concatMap(({ authResponse }) => authResponse ? of(authResponse.accessToken) : EMPTY)
        );
    }

    loginApi(accessToken: string) {
        // authenticate with the api using a facebook access token,
        // on success the api returns an account object with a JWT auth token
        return this.http.post<any>(`${baseUrl}/authenticate`, { accessToken })
            .pipe(map(account => {
                this.accountSubject.next(account);
                this.startAuthenticateTimer();
                return account;
            }));
    }

    logout() {
        FB.logout();
        this.stopAuthenticateTimer();
        this.accountSubject.next(null);
        this.router.navigate(['/login']);
    }

    getAccount() {
        return this.http.get<Account>(`${baseUrl}/current`);
    }
    
    updateAccount(params: any) {
        return this.http.put(`${baseUrl}/current`, params)
            .pipe(map((account: any) => {
                // publish updated account to subscribers
                account = { ...this.accountValue, ...account };
                this.accountSubject.next(account);
                return account;
            }));
    }

    deleteAccount() {
        return this.http.delete(`${baseUrl}/current`)
            .pipe(finalize(() => {
                // auto logout after account is deleted
                this.logout();
            }));
    }

    // helper methods

    private authenticateTimeout?: NodeJS.Timeout;

    private startAuthenticateTimer() {
        // parse json object from base64 encoded jwt token
        const jwtBase64 = this.accountValue!.token!.split('.')[1];
        const jwtToken = JSON.parse(atob(jwtBase64));

        // set a timeout to re-authenticate with the api one minute before the token expires
        const expires = new Date(jwtToken.exp * 1000);
        const timeout = expires.getTime() - Date.now() - (60 * 1000);
        const accessToken = FB.getAuthResponse()?.accessToken;
        if (accessToken) {
            this.authenticateTimeout = setTimeout(() => {
                this.loginApi(accessToken).subscribe();
            }, timeout);
        }
    }

    private stopAuthenticateTimer() {
        // cancel timer for re-authenticating with the api
        clearTimeout(this.authenticateTimeout);
    }
}
 

Plantilla de componente de edición de cuenta

Ruta: /src/app/home/edit-account.component.html

La plantilla del componente de edición de cuenta contiene un formulario para actualizar los detalles de su cuenta o eliminar su cuenta.

<h2>Edit Account</h2>
<p>Updating your info here only changes it in the Angular example app, it does not (and cannot) change anything on Facebook itself.</p>
<form *ngIf="account" [formGroup]="form" (ngSubmit)="saveAccount()">
    <div class="mb-3">
        <label class="form-label">Facebook Id</label>
        <div>{{account.facebookId}}</div>
    </div>
    <div class="mb-3">
        <label class="form-label">Name</label>
        <input type="text" formControlName="name" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.name.errors }" />
        <div *ngIf="submitted && f.name.errors" class="invalid-feedback">
            <div *ngIf="f.name.errors.required">Name is required</div>
        </div>
    </div>
    <div class="mb-3">
        <label class="form-label">Extra Info</label>
        <input type="text" formControlName="extraInfo" class="form-control" />
    </div>
    <div class="mb-3">
        <button type="submit" [disabled]="isSaving" class="btn btn-primary me-2">
            <span *ngIf="isSaving" class="spinner-border spinner-border-sm me-1"></span>
            Save
        </button>
        <a routerLink="/" class="btn btn-secondary me-2">Cancel</a>
        <button (click)="deleteAccount()" class="btn btn-danger" [disabled]="isDeleting">
            <span *ngIf="isDeleting" class="spinner-border spinner-border-sm me-1"></span>
            Delete
        </button>
        <div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
    </div>
</form>
<div *ngIf="!account" class="text-center p-3">
    <span class="spinner-border spinner-border-lg align-center"></span>
</div>
 

Componente de edición de cuenta

Ruta: /src/app/home/edit-account.component.ts

El componente de edición de cuenta permite actualizar los detalles de su cuenta o eliminarla. El formulario se completa previamente con los detalles de la cuenta actual a los que se accede a través de la propiedad accountValue del servicio de cuenta.

Al guardar, la cuenta se actualiza con el servicio de cuenta y el usuario es redirigido a la página de inicio con los detalles de su cuenta.

Al eliminar, la cuenta se elimina de la API de backend y se cierra la sesión del usuario.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService } from '@app/_services';
import { Account } from '@app/_models';

@Component({ templateUrl: 'edit-account.component.html' })
export class EditAccountComponent implements OnInit {
    form!: FormGroup;
    account!: Account;
    isSaving = false;
    isDeleting = false;
    submitted = false;
    error = '';

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            name: ['', Validators.required],
            extraInfo: ['']
        });

        // populate form with current account details
        this.account = this.accountService.accountValue!;
        this.form.patchValue(this.account);
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    saveAccount() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.isSaving = true;
        this.error = '';
        this.accountService.updateAccount(this.form.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.router.navigate(['/']);
                },
                error: error => {
                    this.error = error;
                    this.isSaving = false;
                }
            });
    }

    deleteAccount() {
        this.isDeleting = true;
        this.accountService.deleteAccount()
            .pipe(first())
            .subscribe();
    }
}
 

Plantilla de componente de inicio

Ruta: /src/app/home/home.component.html

La plantilla del componente de inicio contiene un mensaje de bienvenida simple y los detalles de su cuenta con un botón para editar.

<h2>You're logged in with Angular 14 & Facebook!!</h2>
<p>Below are your account details fetched from a secure API endpoint:</p>
<table class="table table-striped">
    <thead>
        <tr>
            <th>Id</th>
            <th>Facebook Id</th>
            <th>Name</th>
            <th>Extra Info</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr *ngIf="account">
            <td>{{account.id}}</td>
            <td>{{account.facebookId}}</td>
            <td>{{account.name}}</td>
            <td>{{account.extraInfo}}</td>
            <td style="width: 50px;">
                <a routerLink="edit-account" class="btn btn-sm btn-primary">Edit</a>
            </td>
        </tr>
        <tr *ngIf="!account">
            <td colspan="5" class="text-center">
                <span class="spinner-border spinner-border-lg align-center"></span>
            </td>
        </tr>
    </tbody>
</table>
 

Componente de inicio

Ruta: /src/app/home/home.component.ts

El componente de inicio obtiene la cuenta actual de la API de backend con el servicio de cuenta en el método ngOnInit() y hace está disponible para la plantilla de inicio a través de la propiedad account.

El componente obtiene la cuenta para demostrar el acceso a un punto final seguro en la API de back-end con el token JWT devuelto después de iniciar sesión. La cuenta también está disponible a través de la propiedad accountValue.

import { Component } from '@angular/core';
import { first } from 'rxjs/operators';

import { AccountService } from '@app/_services';
import { Account } from '@app/_models';

@Component({ templateUrl: 'home.component.html' })
export class HomeComponent {
    account!: Account;

    constructor(private accountService: AccountService) { }

    ngOnInit() {
        this.accountService.getAccount()
            .pipe(first())
            .subscribe(x => this.account = x);
    }
}
 

Plantilla de componente de inicio de sesión

Ruta: /src/app/login/login.component.html

La plantilla del componente de inicio de sesión contiene un solo botón de inicio de sesión de Facebook que está vinculado al método login() del componente de inicio de sesión al hacer clic.

<div class="col-md-6 offset-md-3 mt-5 text-center">
    <div class="card">
        <h4 class="card-header">Angular 14 Facebook Authentication</h4>
        <div class="card-body">
            <button class="btn btn-facebook" (click)="login()">
                <i class="fa fa-facebook me-1"></i>
                Login with Facebook
            </button>
        </div>
    </div>
</div>
 

Componente de inicio de sesión

Ruta: /src/app/login/login.component.ts

El componente de inicio de sesión utiliza el servicio de cuenta para iniciar sesión en la aplicación mediante Facebook. Si el usuario ya ha iniciado sesión, se le redirigirá automáticamente a la página de inicio.

import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent {
    constructor(
        private router: Router,
        private route: ActivatedRoute,
        private accountService: AccountService
    ) {
        // redirect to home if already logged in
        if (this.accountService.accountValue) {
            this.router.navigate(['/']);
        }
    }

    login() {
        this.accountService.login()
            .subscribe(() => {
                // get return url from query parameters or default to home page
                const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
                this.router.navigateByUrl(returnUrl);
            });
    }
}
 

Módulo de enrutamiento de la aplicación

Ruta: /src/app/app-routing.module.ts

El módulo de enrutamiento de la aplicación define las rutas para la aplicación angular y genera un módulo de enrutamiento raíz pasando la matriz de routes al método RouterModule.forRoot(). El módulo se importa al módulo de la aplicación principal a continuación.

La ruta de inicio asigna la ruta raíz (/) de la aplicación al componente de inicio, la ruta de edición de la cuenta /edit-account se asigna al componente de edición de cuenta, y la ruta /login se asigna al componente de inicio de sesión.

Las rutas de la cuenta de inicio y de edición se protegen pasando la auth guard a la propiedad canActivate de cada ruta.

Para obtener más información sobre el enrutamiento angular, consulte https://angular.io/guide/router.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent, EditAccountComponent } from './home';
import { LoginComponent } from './login';
import { AuthGuard } from './_helpers';

const routes: Routes = [
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },
    { path: 'edit-account', component: EditAccountComponent, canActivate: [AuthGuard] },
    { path: 'login', component: LoginComponent },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }
 

Plantilla de componente de aplicación

Ruta: /src/app/app.component.html

La plantilla del componente de la aplicación es la plantilla del componente raíz de la aplicación angular, contiene la barra de navegación principal que solo se muestra cuando ha iniciado sesión y una directiva router-outlet que representa el contenido de cada vista en función de la ruta/camino actual.

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="accountService.accountValue">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
        <button class="btn btn-link nav-item nav-link" (click)="accountService.logout()">Logout</button>
    </div>
</nav>

<!-- main app container -->
<div class="container pt-4">
    <router-outlet></router-outlet>
</div>
 

Componente de la aplicación

Ruta: /src/app/app.component.ts

El componente de la aplicación es el componente raíz de la aplicación angular, define la etiqueta raíz de la aplicación como <app-root></app-root> con la propiedad selector del decorador @Component().

Una instancia de servicio de cuenta se hace accesible a la plantilla de componente de aplicación mediante incluyéndolo como una dependencia en los parámetros del constructor con el modificador protected (protected accountService: AccountService).

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

import { AccountService } from './_services';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    constructor(protected accountService: AccountService) {}
}
 

Módulo de aplicación

Ruta: /src/app/app.module.ts

El módulo de la aplicación define el módulo raíz de la aplicación angular junto con los metadatos sobre el módulo. Para obtener más información sobre los módulos angular 14, consulte https://angular.io/docs/ts/latest/guide/ngmodule.html.

Aquí es donde se agrega el proveedor de backend falso a la aplicación; para cambiar a un backend real, simplemente elimine el fakeBackendProvider que se encuentra debajo del comentario // provider used to create fake backend.

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { JwtInterceptor, ErrorInterceptor, appInitializer } from './_helpers';
import { AccountService } from './_services';
import { HomeComponent, EditAccountComponent } from './home';
import { LoginComponent } from './login';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        EditAccountComponent,
        LoginComponent
    ],
    providers: [
        { provide: APP_INITIALIZER, useFactory: appInitializer, multi: true, deps: [AccountService] },
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
 

Configuración del entorno de producción

Ruta: /src/environments/environment.prod.ts

La configuración del entorno de producción contiene variables necesarias para ejecutar la aplicación en producción. Esto le permite crear la aplicación con una configuración diferente para cada entorno diferente (por ejemplo, producción y desarrollo) sin actualizar el código de la aplicación.

Cuando compila la aplicación para producción con el comando ng build --configuration production, la salida environment.ts se reemplaza por environment.prod.ts.

export const environment = {
    production: true,
    apiUrl: 'http://localhost:4000',
    facebookAppId: '314930319788683'
};
 

Configuración del entorno de desarrollo

Ruta: /src/environments/environment.ts

La configuración del entorno de desarrollo contiene variables necesarias para ejecutar la aplicación en desarrollo.

Se accede a la configuración del entorno importando el objeto del entorno en cualquier servicio Angular del componente con la línea importar {entorno} desde '@environments/environment' y acceder a las propiedades en el objeto environment, consulte el servicio de cuenta para ver un ejemplo.

export const environment = {
    production: false,
    apiUrl: 'http://localhost:4000',
    facebookAppId: '314930319788683'
};
 

Archivo index.html principal

Ruta: /src/index.html

El archivo index.html principal es la página inicial cargada por el navegador que inicia todo. La CLI de Angular (con Webpack bajo el capó) agrupa todos los archivos javascript compilados y los inyecta en el cuerpo de la página index.html para que el navegador pueda cargar y ejecutar los scripts.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 14 - Facebook Authentication Tutorial with Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap & font-awesome css -->
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
    <app-root>Loading...</app-root>
</body>
</html>
 

Archivo main.js

Ruta: /src/main.ts

El archivo main.js es el punto de entrada utilizado por angular para iniciar y arrancar la aplicación.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
 

Polirellenos

Ruta: /src/polyfills.ts

Algunas funciones utilizadas por Angular 14 aún no son compatibles de forma nativa con todos los principales navegadores, los polyfills se utilizan para agregar compatibilidad con funciones cuando sea necesario para que su aplicación Angular funcione en todos los principales navegadores.

Este archivo es generado por Angular CLI al crear un nuevo proyecto con el comando ng new, he excluido los comentarios en el archivo por razones de brevedad.

import 'zone.js';  // Included with Angular CLI.
 

Estilos LESS/CSS globales

Ruta: /src/styles.less

El archivo de estilos globales contiene estilos LESS/CSS que se aplican globalmente en toda la aplicación angular 14.

El ejemplo contiene solo algunos estilos para el botón de inicio de sesión de Facebook.

/* You can add global styles to this file, and also import other style files */
.btn-facebook,
.btn-facebook:hover,
.btn-facebook:first-child:active {
    background: #3B5998;
    border-color: #3B5998;
    color: #fff;
}

.btn-facebook:hover {
    opacity: 0.9;
}

.btn-facebook:first-child:active {
    opacity: 0.8;
}
 

Package.json

Ruta: /package.json

El archivo package.json contiene información de configuración del proyecto, incluidas las dependencias del paquete que se instalan cuando ejecuta npm install y scripts que se ejecutan cuando ejecuta npm start o npm run build etc. La documentación completa está disponible en https://docs.npmjs.com/files/package.json.

{
    "name": "angular-14-example",
    "version": "0.0.0",
    "scripts": {
        "ng": "ng",
        "start": "ng serve --open",
        "start:ssl": "ng serve --ssl --open",
        "build": "ng build",
        "watch": "ng build --watch --configuration development",
        "test": "ng test"
    },
    "private": true,
    "dependencies": {
        "@angular/animations": "^14.2.0",
        "@angular/common": "^14.2.0",
        "@angular/compiler": "^14.2.0",
        "@angular/core": "^14.2.0",
        "@angular/forms": "^14.2.0",
        "@angular/platform-browser": "^14.2.0",
        "@angular/platform-browser-dynamic": "^14.2.0",
        "@angular/router": "^14.2.0",
        "@types/facebook-js-sdk": "^3.3.6",
        "rxjs": "~7.5.0",
        "tslib": "^2.3.0",
        "zone.js": "~0.11.4"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^14.2.8",
        "@angular/cli": "~14.2.8",
        "@angular/compiler-cli": "^14.2.0",
        "@types/jasmine": "~4.0.0",
        "jasmine-core": "~4.3.0",
        "karma": "~6.4.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage": "~2.2.0",
        "karma-jasmine": "~5.1.0",
        "karma-jasmine-html-reporter": "~2.0.0",
        "typescript": "~4.7.2"
    }
}
 

TypeScript tsconfig.json

Ruta: /tsconfig.json

El archivo tsconfig.json contiene la configuración básica del compilador de TypeScript para todos los proyectos en el espacio de trabajo de Angular, configura cómo se compilará/transpilará el código de TypeScript en JavaScript que el navegador pueda entender. Para obtener más información, consulte https://angular.io/config/tsconfig.

La mayor parte del archivo no ha cambiado desde que Angular CLI lo generó, solo se agregó la propiedad paths para asignar @app y @environments alias a los directorios /src/app y /src/environments. Esto permite que las importaciones sean relativas a las carpetas de aplicaciones y entornos al anteponer rutas de importación con alias en lugar de tener que usar rutas relativas largas (por ejemplo, import MyComponent from '@app/MyComponent' en su lugar de importar MyComponent from '../../../MyComponent').

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "allowSyntheticDefaultImports": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": false,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "es2020",
        "module": "es2020",
        "lib": [
            "es2020",
            "dom"
        ],
        "paths": {
            "@app/*": ["src/app/*"],
            "@environments/*": ["src/environments/*"]
        }
    },
    "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true,
        "strictTemplates": true
    }
}

 


Suscríbete o Sígueme para actualizaciones

Suscríbete a mi canal de YouTube o sígueme en Twitter, Facebook o GitHub para recibir notificaciones cuando publique contenido nuevo.

Aparte de la codificación...

Actualmente estoy intentando viajar por Australia en motocicleta con mi esposa Tina en un par de Royal Enfield Himalayan. Puedes seguir nuestras aventuras en YouTube, Instagram y Facebook.


¿Necesita Ayuda Angular 14?

Buscar fiverr para encontrar ayuda rápidamente de desarrolladores Angular 14 experimentados.