Login system with Angular and Firebase – What’s the Angular Zone – My First App in Angular 8

Let’s make a series of entries in which we’ll create an application with Angular 8 and Firebase. The application will be a ToDo App, that is, a task manager app. It will use the Cloud Firestore database, and Firebase Authentication for the login system.

Related entries

  1. Introduction 
  2. Configuring Firebase and Bootstrap in Angular. Understanding Cloud Firestore
  3. Creating our first component – Click event  
  4. Our first reactive form 
  5. Saving and reading data in Cloud Firestore – ngIf and ngFor
  6. Introduction to Routing 
  7. Login system with Angular and Firebase <– You are here
  8. Releasing our Angular App in Azure with VS Code

As I publish the articles, I will be placing the links here above☝.

Login System with Angular and Firebase

We already have an application with which we can read, create, update and delete data. When we enter our application we can see a list of tasks. However, our application has a limitation: It only handles the tasks of a single user. If we publish our application as it is now, users will not be able to have a personalized and private list of tasks. We can solve this by creating a login system.

With a login system, we can authenticate users, so that they are identified, then we can tie each individual task to a specific user. In this way, each user can only see their tasks, without being able to see the tasks of other users.

Enabling authentication in Firebase

With Firebase, creating a user system is pretty straightforward. We have to follow the following steps:

  1. The first thing we must do is go towards our project in Firebase.
  2. Click on Authentication (on the right side of the screen, above Database)
  3. Click on Configure the access method
  4. Enable “Email / password”
  5. If you are going to publish your app, in the “Authorized Domains” section you must place the domain or domains in which you are going to publish your project.

With this, we have our login system configured on the Firebase side. Now, we must work from the side of Angular.

Configuring authentication in Angular

Let’s start by configuring the Angular Fire module related to authentication. We are going to import the AngularFireAuthModule module in our AppModule:


import { AngularFireAuthModule } from '@angular/fire/auth';
@NgModule({
 imports: [
BrowserModule,AngularFireModule.initializeApp(environment.firebaseConfig),
   AngularFirestoreModule, // <—–
   AngularFireAuthModule,
   AppRoutingModule,
   NgbModule,
   ReactiveFormsModule
 ],
})
export class AppModule { }

view raw

app.module.ts

hosted with ❤ by GitHub

This module contains everything necessary to work with authentication in our application. It is through it that we can create our users, authenticate and logout from our app.

We are now going to create a Login component which will allow the user to create an account and login. Let’s execute the following command:

ng generate component login

In the component template let’s write the following code:

login.component.html


<div class="example-container">
 <span class="center">ToDo App</span>
 <ng-container *ngIf="errorMessage">
   <span class="error">{{errorMessage}}</span>
 </ng-container>
 <form [formGroup]="loginForm">
   <div class="form-group">
     <label for="title">Username*</label>
     <input type="text" id="username" placeholder="Username" class="form-control" formControlName="username" />
     <span style="color:Red;"
       ngIf="loginForm.controls['username'].touched && loginForm.controls['username'].errors?.required">Username is
       required.</span>
   </div>
   <div class="form-group">
     <label for="description">Password*</label>
     <input id="password" type="password" placeholder="Password" class="form-control" formControlName="password" />
     <span style="color:Red;"
       ngIf="loginForm.controls['password'].touched && loginForm.controls['password'].errors?.required">Password is
       required.</span>
   </div>
   <div class="buttons-container center">
     <button class="btn btn-primary" (click)="signIn()">
       Log in
     </button>
     <button class="btn btn-success" (click)="createUser()">
       Create Account
     </button>
   </div>
 </form>
</div>

As we can see, here we have a form with two fields: username and password. In addition, we have two buttons below, with which we can login or create a new user.

Let’s then work with the CSS of the component:

login.component.css


.example-container {
 overflow: hidden;
 position: absolute;
 width: 100%;
 height: 100%;
 display: flex;
 flexdirection: column;
 justifycontent: center;
 alignitems: center;
 overflow: hidden;
}
.example-container > * {
 width: 25%;
}
.error {
 color: red;
}
.center {
 textalign: center;
}
.buttons-container button {
 marginleft: 5px;
}

This helps us to centralize the form on the screen. Let’s now see the implementation of the login component class:

login.component.ts


import { Component, OnInit, NgZone } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
@Component({
 selector: 'app-login',
 templateUrl: './login.component.html',
 styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
 errorMessage = '';
 constructor(private afAuth: AngularFireAuth,
   private router: Router,
   private fb: FormBuilder,
   private ngZone: NgZone) { }
 loginForm = this.fb.group({
   email: ['', Validators.required],
   password: ['', Validators.required]
 })
 ngOnInit() {
   this.afAuth.user.subscribe(user => {
     if (user) {
       this.ngZone.run(() => {
         this.router.navigate(['/todos']);
       })
   })
 createUser() {
  this.afAuth.auth.createUserWithEmailAndPassword(this.loginForm.value.email, this.loginForm.value.password).then(() => {
     this.router.navigate(['/todos']);
   }).catch(response => {
     this.errorMessage = response.message;
   });
 signIn() { 
this.afAuth.auth.signInWithEmailAndPassword(this.loginForm.value.email, this.loginForm.value.password).then(() => {
     this.router.navigate(['/todos']);
   }).catch(response => {
     this.errorMessage = response.message;
   });
}

Let’s analyze this code by parts:


constructor(private afAuth: AngularFireAuth,
   private router: Router,
   private fb: FormBuilder,
   private ngZone: NgZone) { }

In the constructor we inject a few services: AngularFireAuth, Router, FormBuilder and NgZone. We already know FormBuilder. Let’s talk about others:

  • AngularFireAuth is the service that helps us work with Firebase authentication in our Angular app.
  • Router helps us to help the user navigate our app with JavaScript code
  • NgZone helps us to enter the Angular area. This is useful when we use functionality that has taken us out of the Angular execution context and we need to go back to it. For example: When we use some libraries that make HTTP calls, for a performance issues, they leave the Angular zone, then, when their callback is executed, we need to use NgZone to re-enter the Angular zone to be able to do something from our Angular application in the callback.

In the ngOnInit of the class we use these three services to verify if the user is logged in, if they are, we enter the Angular zone and then redirect the user to the todo-list route:


ngOnInit() {
   this.afAuth.user.subscribe(user => {
     if (user) {
       this.ngZone.run(() => {
         this.router.navigate(['/todos']);
     })
})

With user.subscribe we can ask Firebase what the user’s current status is. If the user variable is null or undefined, then the user is not logged in, otherwise he is logged in, so in that case we use ngZone.run to enter the Angular zone. Finally, we use router.navigate to navigate the user to the path /todos.

Below we have the method to register a user in our app:


createUser() {
   this.afAuth.auth.createUserWithEmailAndPassword(this.loginForm.value.email, this.loginForm.value.password).then(() => {
     this.router.navigate(['/todos']);
   }).catch(response => {
     this.errorMessage = response.message;
});

As we can see, we pass the user’s email and password to perform the registration. In the same way we have the signIn method to authenticate the user.

We must then go to the AppRoutingModule to create our new route todos:


const routes: Routes = [
 { path: '', component: LoginComponent },
 { path: 'todos', component: TodoListComponent },
 { path: 'contact', component: ContactComponent }
];

We can run the application and verify that we can create our account and we are then redirected to our list of tasks. Now we must make each task tied to a user, so that only the tasks of the authenticated user are displayed.

The first thing we have to do is place the userId property in our Todo model:


export interface Todo {
   title: string;
   description: string;
   done: boolean;
   createdDate: Date;
   lastModifiedDate: Date;
   userId: string; // <–
}

view raw

Todo.ts

hosted with ❤ by GitHub

Then, we must assign the value of userId when creating a task. For that, let’s go to our todo-form component and make some changes. Let’s start by injecting AngularFireAuth to the component class:


constructor(private formBuilder: FormBuilder,
   public activeModal: NgbActiveModal,
   private todoService: TodoService,
   private afAuth: AngularFireAuth) { }

Then, in the ngOnInit, let’s retrieve the authenticated user and save it in a property:


user: User;
ngOnInit() {
   this.todoForm = this.formBuilder.group({
     title: ['', Validators.required],
     description: ['', Validators.required],
     done: false
   });
   if (!this.createMode) { this.loadTodo(this.todo); }
   this.afAuth.user.subscribe(user => {
if (user){
this.user = user;
}
})
}

Where User comes from:

import { User } from ‘firebase’;

Now, let’s go to our saveTodo method and fill in the userId property with the authenticated user Id:


if (this.createMode) {
     let todo: Todo = this.todoForm.value;
     todo.lastModifiedDate = new Date();
     todo.createdDate = new Date();
     todo.userId = this.user.uid; // <—-
     this.todoService.saveTodo(todo)
       then(response => this.handleSuccessfulSaveTodo(response, todo))
       catch(err => console.error(err));

Now, we must make sure that when we search for documents to read, we only bring the ones that correspond to the authenticated user. For that, let’s go to the class of the todo-list component and place the following:


export class TodoListComponent implements OnInit {
 user: User;
 constructor(private modalService: NgbModal,
   private todoService: TodoService,
   private afAuth: AngularFireAuth) { }
 ngOnInit() {
   this.afAuth.user.subscribe(user => {
     if (user) {
       this.loadTodos(user.uid);
   })
 loadTodos(userId: string) {
   this.todoService.getTodos(userId).subscribe(response => {

Note that I only placed the part of the code that changed, in this case: we injected the AngularFireAuth service, we looked for the authenticated user, and in loadTodos we received the ID of the user from whom we want to search the tasks. Finally, we must modify the getTodos method of our TodoService:


getTodos(userId: string): Observable<firebase.firestore.QuerySnapshot> {
 return this.db.collection<Todo>(this.todoCollectionName, ref =>
ref.where("userId", "==", userId).orderBy('lastModifiedDate', 'desc'))
.get();
}

view raw

todo.service.ts

hosted with ❤ by GitHub

As we can see, what we do in getTodos is to receive the user’s Id as a parameter, and place in the ref a where to indicate the filter that we want to apply, in this case, we want to bring all the documents that have the userId field with the value of the authenticated user Id.

If we save, and run our application, we can see in the Chrome console that we have an error (this is normal). The error tells us that we need to create an index to execute this query, and it offers us a link to create it. If you click on the link, you will be taken to Firebase, and you will get a modal like the following (likely in english):

Crear indice compuesto

Click on Create index. After a few minutes, the index will be created. If you go back to your Angular app, and press F5, you’ll see that the error is no longer in the Chrome console.

We can now create tasks which will be associated with the authenticated user. However, in order to test this, we need to have a mechanism to allow the user to logout, in addition, we want the user to be able to send their email in the upper right part of the screen, and we also want the upper menu to only appear when the user is logged in. . For that, let’s go to the app component class and do the following:


export class AppComponent implements OnInit {
 user: User; 
 constructor(private afAuth: AngularFireAuth,
   private router: Router,
   private ngZone: NgZone){}
 ngOnInit(){
   this.afAuth.user.onAuthStateChanged(user => {
     if (user){
       this.user = user;
     } else{
  this.user = null;
       this.ngZone.run(() => {
         this.router.navigate(['']);
       })
   });
}

Here what we do is save the authenticated user in a property of the class. If the user is not authenticated, we redirect him to the login component. Notice that we are also using onAuthStateChanged to indicate that we want to execute the callback every time the user’s authentication status changes in the app: This means that this function will be executed when the component is loaded, when the user logs in or when the user logs out.

In the template of the component we are going to pass the user to the menu component:


<app-menu [user]="user" *ngIf="user"></app-menu>
<div class="container-fluid">
   <router-outlet></router-outlet>
</div>

We see that we are passing the user to the menu component with [user]=”user”, in addition, we are indicating that the menu will only be visible if the user variable is defined.

Let’s then work with the menu component. We will start with its class:


export class MenuComponent {
Input()
user: User;
constructor(private afAuth: AngularFireAuth) { }
logout() {
this.afAuth.auth.signOut();
}

As we can see, we use the @Input attribute to indicate that the user variable is going to be injected by the parent component. In addition, we see that in the logout method we help the user to log out. Finally, we see the template of the menu component:


<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Todo App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" routerLink="">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="contact">Contact</a>
</li>
</ul>
<div class="my-2 my-lg-0">
<div ngbDropdown class="d-inline-block">
<button class="btn btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>{{ user.email }}</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button ngbDropdownItem (click)="logout()">Logout</button>
</div>
</div>
</div>
</div>
</nav>

What we do is use a ng-bootstrap DropDown where we give the user the option of log out. We can run the application and test it. We can log out, log in, create tasks, and the tasks that we create will be associated with the authenticated user, so that if we log out and log in with another user, we will have a new list of tasks for the new user.

Our application is ready to be published in production. We have several options where to publish our app. We are going to do it in an Azure App Service in the next and last entry of this series.

2 comments

  1. The trouble starts at login.component.ts where the snippet has syntax errors. Kind of hard to trouble shoot and follow along.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s