Next-Level Authentication: Leveraging Supabase as a PostgreSQL Database Service for User Authentication with Angular Guard (Part 19) in Your Angular-15 Ionic-7 App

 Welcome back to our ongoing series on building a multiplatform application with Angular-15 and Ionic-7! In our previous articles, we explored a wide range of topics, including authentication, data management, task organization, calendar integration, event management, media management, and UI customization. Today, in Part 19, we're taking our app's authentication system to the next level by incorporating Supabase as a PostgreSQL database service and leveraging Angular Guard for enhanced user authentication.

A robust and secure authentication system is a critical aspect of any application. By integrating Supabase into our Angular-15 Ionic-7 app, we can utilize its powerful features to handle user authentication, database management, and data security. Additionally, by incorporating Angular Guard, we can implement fine-grained access control and ensure that only authorized users can access specific routes within our app.


In this installment, we'll guide you through the process of integrating Supabase into our app as a PostgreSQL database service for user authentication. We'll cover the initial setup, including creating a Supabase account, configuring the necessary settings, and establishing the connection between our app and Supabase.

Next, we'll explore Angular Guard and its role in enforcing authentication rules and route protection. We'll demonstrate how to implement authentication guards to prevent unauthorized access to certain routes within our app. By leveraging Angular Guard in conjunction with Supabase, we'll create a secure and seamless authentication system that enhances the overall user experience.

Throughout this tutorial, we'll emphasize best practices for user authentication, data management, and security. By the end of this article, you'll have a solid understanding of how to integrate Supabase as a PostgreSQL database service for user authentication in your Angular-15 Ionic-7 app and how to leverage Angular Guard for route protection.

So, join us in Part 19 of our series as we embark on a journey to enhance our app's authentication system using Supabase and Angular Guard. Together, let's create a secure and seamless user authentication experience, ensuring that only authorized users can access protected routes within our app.

Tutorial

Till now, I have used MySQL database, and a NoSQL database i.e. Firebase to perform CRUD operations. Now, I will use Supabase, a PostgreSQL database service:

The Open Source Firebase Alternative | Supabase

The steps to set up are similar to Firebase, first I will create an account on Supabase, then add a new app to the console. I will use the API | Supabase documentation to build the authentication feature. Now I will copy the project URL and API key from the project settings in the supabase console as demonstrated in Figure 77 below:

Figure 77: Project API settings in supabase console

Figure 77: Project API settings in supabase console

And past them in the environment.ts file in the project in the following way:

export const environment = {
  production: false,
  //supabase API key - supabase is a postgres database with a rest api  - <https://supabase.io/npc>
  supabaseUrl: 'https://YOUR_URL.supabase.co',
  supabaseKey: 'YOUR_API_KEY',
};

Once the API keys are in place, I will install the supabase client with the command:

npm install --save @supabase/supabase-js

To demonstrate the authentication and CRUD operations, I will create two pages, an achievements list page by ionic generate page pages/list and a login page which is the list-login page by ionic generate page pages/list-login that will act as an authentication guard for the achievement list page.

Before creating a service to communicate with supabase for authentication, I will first create an authentication guard to prevent the user to access the achievement list page without authentication. I will create the service by ionic generate guard guards/auth where I will write the logic to check if the user is authenticated or not.

Before writing the logic in the auth.guard.ts file, I will first create a service via ionic generate service services/supabase . In supabase.service.ts file, I will write the logic to check if the user is logged in, login and sign up in the following way:

import { Injectable } from '@angular/core';
import { createClient, SupabaseClient, User } from '@supabase/supabase-js';
import { BehaviorSubject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class SupabaseService {
  supabase: SupabaseClient; // define supabase as a SupabaseClient type variable (from @supabase/supabase-js)

  private _currentUser: BehaviorSubject<any> = new BehaviorSubject<any>(null); // this is a BehaviorSubject from rxjs that is used to store the current user and is initialized with null as the default value

  // Try to recover our user session
  async ngOnInit() {
    await this.loadUser();
  }

  constructor(private router: Router) {
    // initialize supabase with the environment variables
    this.supabase = createClient(
      // createClient is a function from @supabase/supabase-js that is used to initialize supabase
      environment.supabaseUrl, // environment variables from src/environments/environment.ts
      environment.supabaseKey,
      {
        auth: {
          // auth is an object from @supabase/supabase-js that is used to store the logged-in session
          autoRefreshToken: true, // autoRefreshToken is a boolean from @supabase/supabase-js that is used to refresh the token for logged-in users
          persistSession: true, // persistSession is a boolean from @supabase/supabase-js that is used to store the logged-in session
        },
      }
    );

    // Try to recover our user session
    this.loadUser();

    this.supabase.auth.onAuthStateChange((event, session) => {
      if (event == 'SIGNED_IN') {
        this._currentUser.next(session?.user);
      } else {
        this._currentUser.next(false);
      }
    });
  }

  async loadUser() {
    const user = await this.supabase.auth.getUser();
    if (user) {
      this._currentUser.next(user);
    } else {
      this._currentUser.next(false);
    }
  }

  get currentUser(): Observable<User> {
    return this._currentUser.asObservable();
  }

  get currentUserValue(): User | null {
    return this._currentUser.value;
  }

  async signUp(credentials: { email: string; password: string }) {
    return new Promise(async (resolve, reject) => {
      const { error, data } = await this.supabase.auth.signUp(credentials);
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  }

  signIn(credentials: { email: string; password: string }) {
    return new Promise(async (resolve, reject) => {
      const { error, data } = await this.supabase.auth.signInWithPassword(
        credentials
      );

      //pass the data to the currentUser BehaviorSubject
      //! This was the final issue that was preventing the user from being logged in
      //? The data was not being passed to the currentUser BehaviorSubject
      //? This was because the data was not being passed to the currentUser BehaviorSubject
      //? The user was able to access the list page even though they were not logged in
      this._currentUser.next(data?.user); // pass the user to the currentUser BehaviorSubject

      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  }

  async signOut() {
    await this.supabase.auth.refreshSession(); // refreshSession is a function from @supabase/supabase-js that is used to refresh the session
    await this.supabase.auth.signOut(); // signOut is a function from @supabase/supabase-js that is used to sign out a user
    // set the current user to null after signing out
    this._currentUser.next(null);
    this.router.navigateByUrl('/tabs/list-login'); // redirect to the login page after signing out
    this.supabase.auth.onAuthStateChange((event, session) => {
      console.log('event', event);
      console.log('session', session);
    });
  }

  isLoggedIn() {
    // this function is used to check if the user is logged in which will be used in auth.guard.ts to protect the routes from unauthorized access

    const user = this._currentUser.getValue(); // get the current value of the BehaviorSubject
    console.log('user: ', user);
    return !!user; // if user is not null or undefined, return true else return false
  }
}

In the above code, there are methods for user authentication and session management, including signUp(), signIn(), signOut(), and isLoggedIn(). It also defines a currentUser observable and currentUserValue getter to retrieve the current user information. The service uses BehaviorSubject from the rxjs library to store the current user information and emit updates to subscribers whenever the user signs in or signs out. When the service is initialized, it loads the current user session and sets up an event listener to handle changes in the user's authentication state. createClient() is used to initialize Supabase with the environment variables, and auth is used to store the logged-in session.

loadUser() retrieves the current user using getUser() and sets the currentUser BehaviorSubject to the user object if it exists, or false if it does not. signUp() and signIn() use Promise to handle the asynchronous Supabase API calls and update the currentUser BehaviorSubject with the logged-in user information. signOut() refreshes the session using refreshSession() and signs the user out using signOut(). It also sets the currentUser BehaviorSubject to null and redirects the user to the login page.

isLoggedIn() checks if the current user information exists and returns a boolean indicating whether the user is currently logged in.

Next I will use the isLoggedIn() method and write the code for the auth.guard.ts in the following way:

import { Injectable } from '@angular/core';
import { UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { SupabaseService } from '../services/supabase.service';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard {
  constructor(
    private supabaseService: SupabaseService,
    private router: Router
  ) {}
  canActivate():
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    if (this.supabaseService.isLoggedIn()) {
      // this allows the user to access the page if they are logged in
      return true;
    } else {
      this.router.navigateByUrl('/tabs/list-login'); // this redirects the user to the login page if they are not logged in
      return false;
    }
  }
}

The above code defines an AuthGuard class that implements the CanActivate interface from Angular which is an interface that defines a guard which decides if a route can be activated. The AuthGuard is used to protect routes from unauthorized access. The AuthGuard class has a constructor that takes in two parameters: supabaseService of type SupabaseService and router of type Router. SupabaseService is a service that handles user authentication using Supabase, while Router is used to navigate to different routes in the application.

The canActivate method is implemented to check if the user is logged in. If the user is logged in, the method returns true, which allows the user to access the protected route. If the user is not logged in, the method redirects the user to the login page and returns false, which prevents the user from accessing the protected route. To check if the user is logged in, the canActivate method calls the isLoggedIn() method from SupabaseService. If the isLoggedIn() method returns true, it means the user is logged in and the method returns true. If the isLoggedIn() method returns false, it means the user is not logged in, and the method redirects the user to the login page using the router.navigateByUrl() method and returns false.

I will use this guard in tabs.route.ts in the following way:

import { AuthGuard } from '../../guards/auth.guard';

export const routes: Routes = [
  {
    path: 'tabs',
    component: TabsPage,
    children: [
     ...
      {
        path: 'list',
        loadComponent: () =>
          import('../list/list.page').then((m) => m.ListPage),
        canActivate: [AuthGuard],
      },
      {
        path: 'list-login',
        loadComponent: () =>
          import('../list-login/list-login.page').then((m) => m.ListLoginPage),
      },
...
    ],
  },
  {
    path: '',
    redirectTo: '/tabs/tab1',
    pathMatch: 'full',
  },
];

In the above code, the AuthGuard is applied to the ListPage route using the canActivate property, which means that the AuthGuard will check if the user is logged in before allowing them to access the ListPage component.

Next, I will write the logic for sign up and login in the list-login.page.ts file in the following way:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators,} from '@angular/forms';
import { AlertController, ToastController, IonicModule, LoadingController,
} from '@ionic/angular';
import { SupabaseService } from 'src/app/services/supabase.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-list-login',
  templateUrl: './list-login.page.html',
  styleUrls: ['./list-login.page.scss'],
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule, ReactiveFormsModule],
})
export class ListLoginPage implements OnInit {
  credentials!: FormGroup;

  constructor(
    private supabaseService: SupabaseService,
    private fb: FormBuilder,
    private alertController: AlertController,
    private router: Router,
    private loadingController: LoadingController,
    private toastController: ToastController
  ) {}

  ngOnInit() {
    this.credentials = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
    });
  }

  async supabaseLogin() {
    const loading = await this.loadingController.create();
    await loading.present();

    this.supabaseService
      .signIn(this.credentials.value)
      .then((res) => {
        loading.dismiss();
        //show login success
        this.showError('Login Successful', 'Welcome back!');

        //create a toast message to show login success
        this.toastController
          .create({
            message: 'Login Successful',
            duration: 2000,
          })
          .then((toast) => toast.present());

        this.router.navigateByUrl('/tabs/list', { replaceUrl: true }); //navigate to achievement list page
      })
      .catch((err) => {
        loading.dismiss();
        this.showError('Login Failed', err.message);
      });
  }

  async signUp() {
    const loading = await this.loadingController.create();
    await loading.present();

    this.supabaseService
      .signUp(this.credentials.value)
      .then((res) => {
        loading.dismiss();
        this.showError('Sign up successful', 'Please Confirm your email');
        // this.router.navigateByUrl('/list', { replaceUrl: true });
      })
      .catch((err) => {
        loading.dismiss();
        const alert = this.alertController.create({
          header: 'Sign up failed',
          message: err.message,
          buttons: ['OK'],
        });
        alert.then((alert) => alert.present());
      });
  }

  async showError(title: string, msg: string) {
    const alert = await this.alertController.create({
      header: title,
      message: msg,
      buttons: ['OK'],
    });
    await alert.present();
  }
}

The above code defines a class has a property credentials that is an instance of FormGroup from @angular/forms package. The constructor of the class takes several dependencies injected as arguments such as SupabaseService, FormBuilder, AlertController, Router, LoadingController, and ToastController. The ngOnInit() function initializes the credentials property with a FormGroup that has two form controls email and password. Both the form controls have some validators applied to them.

The supabaseLogin() function is called when the user clicks on the login button on the page. This function first creates a loading spinner to indicate the ongoing process and then calls the signIn() function of the SupabaseService to authenticate the user. If the authentication is successful, it shows a success message using a toast message, navigates the user to the list page, and dismisses the loading spinner. If the authentication fails, it shows an error message using an alert dialog and dismisses the loading spinner.

The signUp() function is called when the user clicks on the sign-up button on the page. This function first creates a loading spinner to indicate the ongoing process and then calls the signUp() function of the SupabaseService to register the user. If the registration is successful, it shows a success message using an alert dialog, and dismisses the loading spinner. If the registration fails, it shows an error message using an alert dialog, and dismisses the loading spinner.

The showError() function is a helper function to show an error message using an alert dialog. It takes two arguments title and msg as the title and message of the alert dialog, respectively, and displays the alert dialog to the user.

Next, I will use the above logic to build the template file list-login.page.html in the following way:

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>View Achievements</ion-title>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/tabs/tab1"></ion-back-button>
    </ion-buttons>
    <ion-buttons slot="end">
      <ion-menu-button menu="main-menu"></ion-menu-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <form (ngSubmit)="supabaseLogin()" [formGroup]="credentials">
    <div class="input-group">
      <ion-list lines="none">
        <ion-item>
          <ion-label>Login/SignUp to View Achievements</ion-label>
        </ion-item>
        <ion-item>
          <ion-input
            class="ion-margin-top"
            type="email"
            label="email"
            aria-label="email"
            label-placement="floating"
            fill="outline"
            placeholder="john@doe.com"
            formControlName="email"
          ></ion-input>
        </ion-item>
        <ion-item>
          <ion-input
            class="ion-margin-top"
            label="password"
            aria-label="password"
            label-placement="floating"
            fill="outline"
            type="password"
            placeholder="password"
            formControlName="password"
          ></ion-input>
        </ion-item>
      </ion-list>
    </div>
    <ion-button
      class="ion-margin"
      type="submit"
      expand="full"
      fill="solid"
      shape="round"
      color="primary"
      [disabled]="!credentials.valid"
      >Log in</ion-button
    >
    <ion-button
      class="ion-margin"
      type="button"
      expand="full"
      fill="outline"
      shape="round"
      (click)="signUp()"
      color="primary"
      >Sign Up
    </ion-button>
  </form>
</ion-content>

In the above code, the form uses the Angular directive ngSubmit to bind the supabaseLogin() function to the submit event. The form group credentials is also specified with its form controls email and password, each of which is bound to an ion-input element using formControlName.

The form also includes two ion-button elements, one for submitting the form and one for signing up. The submit button is disabled until the form is valid.

And with this, I have successfully implemented Supabase as the authentication provider with a functional authentication guard as demonstrated in the figures below:

Figure 78: Login Page


Figure 79: Form Input Validation



Figure 80: Login Successful


Conclusion

In this nineteenth installment of our series on building a multiplatform application with Angular-15 and Ionic-7, we explored the integration of Supabase as a PostgreSQL database service for user authentication, along with the implementation of Angular Guard for enhanced route protection. By leveraging these powerful tools, we created a robust and secure authentication system that ensures only authorized users can access specific routes within our app.

User authentication and data security are critical aspects of any application. By incorporating Supabase into our Angular-15 Ionic-7 app, we harnessed its capabilities to handle user authentication, database management, and data security seamlessly. Additionally, by utilizing Angular Guard, we implemented fine-grained access control, allowing us to protect sensitive routes and provide a secure user experience.

Throughout this tutorial, we guided you through the process of integrating Supabase as a PostgreSQL database service for user authentication. We covered the initial setup, configuration, and establishment of the connection between our app and Supabase. We also explored the implementation of Angular Guard and its role in enforcing authentication rules and route protection.

By following best practices for user authentication, data management, and security, we created a robust authentication system that enhances the overall user experience. Our users can now securely access protected routes within our app, ensuring their data remains safe and confidential.

We hope this tutorial has provided you with valuable insights and practical knowledge on integrating Supabase and Angular Guard into your Angular-15 Ionic-7 app for user authentication and route protection. By embracing these tools, you can create a secure and seamless authentication system that enhances the overall security and user experience of your application.

Thank you for joining us on this journey as we explored the fascinating world of app development with Angular-15, Ionic-7, Supabase, and Angular Guard. Stay tuned for future installments where we will continue to explore new features and functionalities to make our app even more robust, secure, and user-friendly.

Happy coding!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript