Skip to content

Latest commit

 

History

History
415 lines (317 loc) · 13.6 KB

README.md

File metadata and controls

415 lines (317 loc) · 13.6 KB

The Firebase PWA Kit

The Firebase PWA Kit is an all in one guide on how to build secure, valuable, fast web apps with Ionic, Angular and Firebase. This kit is a combination of steps that you can follow, code snippets and packages that you'll need to get it all done.

This guide is ideal for web developers who are familiar with Ionic/Angular and want to get up and running with Firebase. My goal is to help you initialise a secure, progressive web app using Firebase.

What you will learn

  • Initialising an Ionic App with Angular
  • Adding PWA Elements
  • AngularFire & Firebase
  • User Authentication
  • Auth Route Guards
  • Firestore Rules
  • Deploy PWA to Hosting

What you will need

  • A Computer & Internet
  • Firebase Project
  • VSCode or other text editor
  • 15 - 30 Minutes of Time

Sections

  • Initialisation
  • Firebase Connection
  • Deployment

Ionic Initialisation

Install Ionic CLI Globally

  npm i -g @ionic/cli

Create a new Ionic App with Angular. Select the blank template and select Angular as the Framework

  ionic start

Output

Install dependencies

  npm i @angular/fire firebase --save

Navigate into the App Directory

  cd <app-name>

Start the server

  ionic serve

Adding PWA Elements

You now have a Ionic web app but let's turn into a PWA with one magic angular command:

    ng add @angular/pwa

It adds different png files for different splash images for various resolutions icon-128x128.png, icon-144x144.png, icon-152x152.png, icon-192x192.png, icon-384x384.png, icon-512x512.png. Additionally, it adds ngsw-config.json and manifest.webmanifest for configuration purposes. Also, it modifies angular.json, package.json, index.html and app.module.ts. So now when your app is served users will be prompted to install the PWA upon vising your app. Output

Firebase Initialisation

Once you have a project setup go ahead and click the web icon to get your config. Alternatively you can find it by clicking the settings icon in the top left and selecting ptoject settings.

Output

Output

Let's grab that config object and and bring it to both your environments file in the ionic app.

src/app/environments/environments.ts and environments.prod.ts

  firebaseConfig = {
    apiKey: <key>,
    authDomain: <authDomain>,
    projectId: <projectId>,
    storageBucket: <bucket>,
    messagingSenderId: <senderId>,
    appId: <appId>
  }

Once you have your firebase config object added to your environments files you almost ready to go. Up next we need to configure the AngularFire pacakage in necessary modules to interact with firebase from our Ionic/Angular app.

Let's jump to our app module at src/app/app.module.ts and add the following imports:

    // First we bring in our environments file
    import { environment } from '../environments/environment';

    // Then we bring in the AngularFireModule which we'll need shortly
    import { AngularFireModule } from '@angular/fire/compat';

    // We're also going to bring in the Firestore Database module to interact with Firestore
    import { AngularFirestoreModule } from '@angular/fire/compat/firestore';

Now let's bring those imports into the imports array in your app module.

  imports: [
    ...
    // initialize the firebase app
    AngularFireModule.initializeApp(environment.firebaseConfig),

    // initialize the Firestore module with offline persistence
    AngularFirestoreModule.enablePersistence()
  ]

Firestore Usage in page component (eg: app.component.ts):

  // Import the AngularFirestore package
  import { AngularFirestore } from '@angular/fire/compat/firestore';

  // Use the package via dep injection
  constructor(
    private db: AngularFirestore
  ) {}

  // Read a collection
  readCollection() {
    this.db.collection('my-collection')
      // Listen to changes to collection
      .valueChanges()
      // Retrieve data as observable
      .subscribe(data => console.log(data));
  }

  // Add data to collection
  addData() {
    // Document to add to collection
    const myData = {
      name: 'John',
      surname: 'Doe'
    };

    this.db.collection('my-collection')
      .add(myData)
      // Added Successfully
      .then(docRef => console.log(docRef))
      // Uh Oh. 
      .catch(err => console.log(err));
  }

Congrats. Now you have your app connected to firebase and you can start using it. To see if your documents have successfully been stored check the Firebase console.

Firebase Authentication

So now you have Firebase installed and configured. At this point in time I would recomment that you jump right into setting up Authentication. There are many ways to achieve this however we will make use of pages, services and guards to achieve our desired auth flow.

Luckily for us we can employ the Ionic CLI. Please run the following commands to generate the relevant pages, services and page guards.

  // This will create an auth page directory in your src and add the relevant auth page files
  ionic g page auth/auth 

  // This will generate a auth guard inside the auth directory. auth.guard.ts 
  ionic g guard auth/auth

  // Up next we'll generate the service auth.service.ts inside our auth directory
  ionic g service auth/auth

With that all done, we have the necessary files required to move onto the next step. Before we do let me explain the purpose of these files. The first is our auth page, this will house our UI and Component code, the second is a page guard that we will make use of to only allow authenticated users to a specific page route and lastly we have our auth service which is where we will house all the important auth code to be injected into other components later.

Before we can move forward we will need a User Model. Let's create a file for it in our auth folder:

user.model.ts

    export interface User {
        uid: string;
        email: string;
        photoURL?: string;
        displayName?: string;
        returnedAt: any;
        roles?: Roles;
    }

    export interface Roles {
        visitor?: boolean;
        subscriber?: boolean;
        admin?: boolean;
    }

On to the auth service:

auth.service.ts

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

    import auth from 'firebase/compat/app';
    import { AngularFireAuth } from '@angular/fire/compat/auth';
    import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/compat/firestore';

    import { Observable, of } from 'rxjs';
    import { first, switchMap, take } from 'rxjs/operators';

    @Injectable({ providedIn: 'root' })
    export class AuthService {

        user$: Observable<User>;

        constructor(
        private afAuth: AngularFireAuth,
        private afs: AngularFirestore,
        private router: Router,
        ) { 
            // Get the auth state, then fetch the Firestore user document or return null
            this.user$ = this.afAuth.authState.pipe(
                switchMap(user => {
                    // Logged in
                if (user) {
                    return this.afs.doc<User>(`users/${user.uid}`).valueChanges();
                } else {
                    // Logged out
                    return of(null);
                }
                })
            )
        }

        getUser() {
            return this.user$.pipe(first()).toPromise();
        }

        async googleSignin() {
            const provider = new auth.auth.GoogleAuthProvider();
            const credential = await this.afAuth.signInWithPopup(provider);
            return this.updateUserData(credential.user);
        }
    
        private updateUserData(user) {

            // Sets user data to firestore on login
            const userRef: AngularFirestoreDocument<User> = this.afs.doc(`users/${user.uid}`);

            const data = { 
                uid: user.uid, 
                email: user.email, 
                displayName: user.displayName, 
                photoURL: user.photoURL,
                returnedAt: new Date(),
                roles: {
                    visitor: true,
                    subscriber: false,
                    admin: false
                }
            };

            return userRef.set(data, { merge: true });
        }
    
        async signOut() {
        this.afAuth.signOut().then(() => {
            this.router.navigate(['/auth']).then(() => {
            console.log('Redirect To Auth Page');
            }).catch(err => console.log(err));
        });
        }

    }

Let's add a Google Auth button to our html for testing:

auth.page.html

    <ion-button color="dark" expland="block" fill="clear" (click)="auth.googleSignin()">
        <ion-icon name="logo-google" slot="start"></ion-icon> Continue with Google
    </ion-button>

In order to use this auth.googleSignin method we will need to inject it into the page and make it public.

auth.page.ts

    constructor(
        public auth: AuthService
    ) { }

Last but not least let's configure an auth based route Guard. This route guard has just one job which is to only allow authenticated users to certain page routes.

auth.guard.ts

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

    import { AuthService} from './auth.service'
    import { Observable } from 'rxjs';
    import { tap, map, take } from 'rxjs/operators';
    import { AlertController, ToastController } from '@ionic/angular';

    @Injectable({
        providedIn: 'root'
    })
    export class AuthGuard implements CanActivate {
        constructor(
            private auth: AuthService, 
            private router: Router, 
            private alertCtrl: AlertController,
            private toastCtrl: ToastController
        ) {}

        canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {

            return this.auth.user$.pipe(
                take(1),
                map(user => !!user), // <-- map to boolean
                tap(loggedIn => {
                    if (!loggedIn) {
                    this.toastCtrl.create({
                        message: 'Please login first.',
                        duration: 4000
                    }).then(toastEl => toastEl.present()).catch(err => console.log(err));
                    console.log('access denied');
                    this.router.navigate(['/auth']);
                    }
                })
            )
        }
    }

So now we have our guard. At this point you can add it to your routing page module on the routed that you want to protect:

app-routing.module.ts

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

    const routes: Routes = [
        {
            path: '',
            loadChildren: () => import('./pages/auth/auth.module').then(m => m.AuthPageModule)
        },
        {
            path: 'protected-route',
            loadChildren: () => import('./pages/protected-page/protected-page.module').then( m => m.ProtectedPageModule),
            canActivate: [AuthGuard]
        }
    ]

Congrats, you have implemented Firebase auth and managed to get your app working with it. But the work is not yet done. We only need to secure our Firebase database by modifying our Firestore rules.

I have gone ahead and shared some helper functions with you to secure your Firestore collection paths using auth or user roles.

Firestore Rules:

    // Reusable function to determine document ownership
    function isOwner(userId) {
        return request.auth.uid == userId
    }
    
    // Reusable function to get users role
    function getRole(role) {
      return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles[role]
    }

    // Allow create, write, read and update on our users collection
    match /users/{userId} {
        allow create, write, read, update;
    }

Deployment to Firebase Hosting

Congrats on making it this far. The last step for us is to build our PWA and deploy it to Firebase Hosting. For this please ensure that you have hosting enables on the Firebase console.

Let's start by installing the firebase-tools with NPM:

    npm i firebase-tools

Next we want to initialize the Firebase CLI in our project directory:

    firebase init

In the step above you will need to confirm that you are trying to init firebase, then you will need to select hosting in the options menu. When you promted to setup as a SPA say yes. Lastly, make sure that you link it to your www folder where we will place the build app.

Now lets build the production app shall we:

    ionic build --prod

Once this has completed you should now see the www folder in your project directory.

Now that we have out built production app lets deploy it then:

    firebase deploy --only hosting

The above commant will deploy your application to firebase hosting! You should see the testing link in the response, alternatively have a look at the hosting tab in the Firebase console for more info.