YouTube Courses - Learn Smarter

YouTube-Courses.site transforms YouTube videos and playlists into structured courses, making learning more efficient and organized.

Firebase Auth Tutorial

Learn how to set up Firebase Authentication in a web application. This tutorial covers the basics of Firebase, setting up the frontend template, and integrating Firebase Authentication.



Firebase Authentication: An Educational Guide

This chapter introduces Firebase Authentication and its role in modern web application development. We will explore the broader Firebase ecosystem, understand the concept of backend-as-a-service, and delve into the specifics of authentication within this context. This chapter will also set the stage for practical application by outlining a project and the necessary prerequisites for further study.

1. Introduction to Firebase and Backend-as-a-Service

Welcome to the world of Firebase! This course will guide you through the process of becoming proficient in Firebase Authentication, a crucial skill for building interactive and secure web applications. Our primary focus will be on implementing authentication flows within front-end applications, enabling features such as user signup, login, data access control, and personalized user profiles.

To understand the significance of Firebase Authentication, it’s essential to first grasp the overarching concept of Firebase itself.

Firebase is a comprehensive platform offering a suite of backend services designed to simplify web and mobile application development. It provides tools and infrastructure for databases, storage, serverless functions, and more.

Firebase operates as a Backend-as-a-Service (BaaS).

Backend-as-a-Service (BaaS) is a cloud service model that provides developers with pre-built backend infrastructure and functionalities. This allows developers to focus on the frontend and user experience without needing to manage servers, databases, and other backend components from scratch.

In essence, Firebase offers a complete backend infrastructure that can be seamlessly integrated into your web applications. This eliminates the need to build a backend from the ground up in many scenarios, accelerating development and reducing complexity.

2. Firebase Features: A Comprehensive Backend Ecosystem

Firebase encompasses a wide array of features, providing a holistic solution for backend needs. These features, working in concert, constitute a complete backend infrastructure. Let’s examine the key components:

  • Real-time Database:

    A real-time database allows for data to be synchronized instantly across connected clients. This means that when data changes, all users connected to the database receive updates in real-time, without needing to manually refresh or poll for changes.

    This feature enables dynamic and collaborative applications where data consistency across users is paramount.

  • Cloud Storage:

    Cloud storage is a service that allows you to store and access digital data in a network of data servers. This eliminates the need for local storage and provides scalability, accessibility, and data redundancy.

    Firebase Cloud Storage provides a robust solution for storing user-generated content such as images, videos, and documents directly from your applications.

  • Cloud Functions:

    Cloud Functions are serverless compute functions that execute in response to events triggered by Firebase features or HTTPS requests. This allows you to run backend code without managing servers, scaling automatically with demand.

    Cloud Functions enable you to extend the functionality of Firebase, automate tasks, and integrate with third-party services.

  • Hosting:

    Hosting is the service of storing and serving website files on web servers, making them accessible over the internet. Firebase Hosting provides a fast and secure way to deploy and host web applications.

    Firebase Hosting offers global content delivery network (CDN) capabilities, ensuring fast loading times for users worldwide.

  • Authentication:

    Authentication is the process of verifying the identity of a user or device to grant access to a system or resource. This ensures that only authorized users can access protected data and functionalities.

    Firebase Authentication, the focus of this course, provides a comprehensive suite of tools for managing user authentication in your applications. It supports various authentication methods, including email/password, social logins, and phone number authentication.

3. Firebase Authentication in Action: The “Game Guides” Application

To practically demonstrate Firebase Authentication, we will be building an application called “Game Guides.” This application will showcase different levels of user access and permissions, highlighting the power and flexibility of Firebase Authentication.

The “Game Guides” application is designed to provide logged-in users with access to a collection of game guides. Let’s explore its key functionalities:

  • User Roles and Authentication Tiers: The application implements different authentication tiers, defining varying levels of access based on user roles.

    Authentication tiers refer to different levels or categories of access control within an application. These tiers are often based on user roles, permissions, or subscription levels, granting different functionalities or data access to users based on their tier.

    • Unauthenticated Users: Users who are not logged in have no access to the application’s content. They cannot view guides or perform any actions.
    • Authenticated Users (Standard Users): Logged-in users can view all available game guides and access their account information.
    • Admin Users: Admin users possess elevated privileges. In addition to viewing guides and accessing their accounts, they can:
      • Create new game guides.
      • Designate other users as admins.
  • Data Storage with Firestore: The application utilizes Firestore to store all guide data and user information.

    Firestore is a NoSQL document database from Firebase designed for mobile and web application development. It is known for its real-time data synchronization, scalability, and flexible data structuring capabilities.

    Firestore’s real-time capabilities are leveraged to provide immediate updates to the guide list without page refreshes.

    In the context of Firestore, real-time refers to the database’s ability to instantly propagate data changes to all connected clients. This ensures that all users see the most up-to-date information without manual intervention.

  • Admin Console and User Management: Admin users have access to a dedicated section within the application, often referred to as the console, to manage user roles.

    A console, in this context, is a user interface, typically web-based, used to manage and monitor a system or application. It provides tools for configuration, monitoring logs, and performing administrative tasks.

    Through this console, admins can promote standard users to admin status by simply entering their email address. The application uses Firebase Cloud Functions (though not explicitly shown in this introductory demonstration) to handle the backend logic for admin role assignment. The success of this operation is often logged and displayed within the browser’s developer console for debugging and monitoring purposes.

    The browser’s console is a tool available in web browsers, typically accessed through developer tools, that displays logged messages, errors, and allows for the execution of JavaScript code directly within the browser environment.

4. Course Resources and Prerequisites

To facilitate your learning journey, all course files and code examples are available on a dedicated repo on GitHub.

Repo (short for repository) is a central storage location for version-controlled files, often used in software development to track changes and collaborate on code. In the context of this course, it refers to a Git repository hosted on GitHub.

The repo structure is organized into branches for each lesson.

Branches in Git are independent lines of development within a repository. They allow developers to work on new features or bug fixes in isolation without affecting the main codebase.

  • Master Branch: The main branch, often called the master branch, initially appears empty as it serves as the starting point and a reference to the latest stable code.

    The master branch is typically the default and primary branch in a Git repository. It usually represents the stable, production-ready version of the codebase.

  • Lesson Branches: Code specific to each lesson is located on individual branches named accordingly (e.g., “lesson-10,” “lesson-11”). This allows you to access the code at each stage of the course.

For this course, Visual Studio Code is the recommended code editor.

Visual Studio Code is a popular, free source code editor developed by Microsoft. It is widely used for software development across various programming languages and offers features like debugging, syntax highlighting, and integrated terminal.

Additionally, installing the Live Server extension for Visual Studio Code is highly recommended.

Live Server is a Visual Studio Code extension that provides a local development server with live reload functionality. This means that the browser automatically refreshes whenever you save changes to your code, enabling rapid development and instant feedback.

Before embarking on this course, it is assumed that you possess a foundational understanding of the following technologies:

  • JavaScript: A basic understanding of JavaScript fundamentals, including variables, functions, loops, and conditional statements, is essential. If you are new to JavaScript, it is recommended to complete a beginner’s course first.

    JavaScript is a versatile and widely-used programming language primarily employed for front-end web development to add interactivity and dynamic behavior to web pages.

  • HTML: Familiarity with HTML for structuring web page content is expected.

    HTML (HyperText Markup Language) is the standard markup language for creating the structure and content of web pages. It uses tags to define elements like headings, paragraphs, lists, and links.

  • CSS: A basic grasp of CSS for styling web pages is beneficial.

    CSS (Cascading Style Sheets) is a stylesheet language used to describe the presentation of a document written in HTML or XML. It controls aspects like colors, fonts, layouts, and responsiveness.

  • Materialize CSS (Optional): While not mandatory, familiarity with Materialize CSS, a front-end framework, will be helpful as it is used for the application’s template.

    Materialize CSS is a responsive front-end framework based on Google’s Material Design principles. It provides pre-built components and styles to create visually appealing and consistent web interfaces.

  • Firebase Firestore (Recommended): Prior experience with Firebase Firestore or a real-time database is advantageous but not strictly required. The course will briefly cover basic Firestore functionalities. However, for a deeper understanding, exploring dedicated Firestore resources is recommended.

5. Conclusion

This introductory chapter has provided a comprehensive overview of Firebase Authentication and its context within the broader Firebase ecosystem. We have explored the concept of backend-as-a-service, examined key Firebase features, and introduced the “Game Guides” application that will serve as a practical demonstration throughout this course. By understanding these foundational concepts and ensuring you meet the prerequisites, you are well-prepared to embark on your journey to becoming a Firebase Authentication ninja!


Introduction to Firebase Authentication

Firebase is a powerful platform that simplifies web and mobile application development by providing a comprehensive suite of backend services. This chapter will explore how Firebase handles user authentication, a crucial aspect of modern web applications.

Firebase: A comprehensive mobile and web application development platform provided by Google. It offers various tools and services, including databases, authentication, cloud functions, and hosting, simplifying backend development.

1. Firebase as a Backend and the Role of the Firebase SDK

As we know, Firebase acts as a complete backend solution for websites and applications.

Backend: The server-side of an application, responsible for data storage, processing, and logic. It operates behind the scenes and is not directly visible to the user, contrasting with the frontend (user interface).

This means Firebase handles the server-side logic, data storage, and other essential functionalities, allowing developers to focus on building the user interface and user experience on the frontend.

Frontend: The user-facing part of an application, responsible for the user interface and user experience. It’s what users directly interact with, typically built with technologies like HTML, CSS, and JavaScript.

We manage our Firebase backend services through the Firebase Console, a user-friendly web interface provided by Firebase.

Firebase Console: A web interface provided by Firebase for managing Firebase projects and services. It allows users to configure settings, monitor usage, and access various Firebase features.

From the frontend of our application, we connect to this backend using the Firebase SDK (Software Development Kit).

Firebase SDK (Software Development Kit): A set of software development tools provided by Firebase to enable developers to integrate Firebase services into their applications. It includes libraries and APIs for different platforms (web, mobile, etc.).

The Firebase SDK allows us to communicate with various Firebase services, including databases, Cloud Functions, and, most importantly for this chapter, Authentication.

Cloud Functions: Serverless compute environment in Firebase that allows you to run backend code in response to events triggered by Firebase features and HTTPS requests. This enables extending Firebase functionality without managing servers.

Authentication: The process of verifying the identity of a user or device. In Firebase, it manages user sign-up, sign-in, and user identity verification to secure access to application resources.

While this chapter primarily focuses on Firebase Authentication, we will also touch upon Firestore and Cloud Functions in practical examples later on.

Firestore: A NoSQL document database offered by Firebase, designed for mobile and web applications. It is highly scalable, flexible, and provides real-time data synchronization.

2. Understanding the Firebase Authentication Flow

Let’s delve into how the Firebase Authentication process actually works:

  • User Credential Capture: On the frontend of our application, we typically have forms such as login or signup forms. These forms are designed to capture user credentials, like email addresses and passwords.

    Credentials: Information used to verify a user’s identity, typically a username and password. Firebase authentication methods often rely on email/password combinations or social login providers.

  • Secure Credential Transmission: Once the user submits their credentials, these are securely sent to the server (Firebase backend) via methods provided by the Firebase SDK. These methods are typically login or signup methods.

    Server: A computer system that provides services or resources to other computers (clients) over a network. In web development, it typically hosts the backend logic and data of an application.

  • Credential Validation and Token Generation: On the Firebase server, the received credentials are validated. Upon successful validation, Firebase generates an authentication token. This token is then sent back to the user’s browser.

    Authentication token: A cryptographically signed piece of data that verifies a user’s identity after successful authentication. It is used to authorize access to protected resources and services.

  • Frontend Data Access with Token: This authentication token allows the frontend to access user-specific data, such as the user’s name or email address, directly from the token itself.

  • Token-Based Authorization for Backend Requests: When the user makes subsequent requests to Firebase services, like Firestore or Cloud Functions, this authentication token is automatically sent along with the request.

  • Service-Side Authorization: Firebase services like Firestore and Cloud Functions can then inspect this token to identify the user making the request. This enables service-side authorization, allowing you to protect data based on user identity and roles. For example, you can implement security rules in Firestore to restrict access to sensitive data only to users with administrator roles, determined by information within the token.

  • User State Persistence: Once a user is successfully logged into the application, Firebase automatically persists the user’s state.

    Persist user state: Maintaining a user’s logged-in status across sessions or page refreshes. Firebase handles this automatically, so users don’t need to log in every time they revisit the application.

    This persistence is a valuable feature, as it ensures that users remain logged in even after refreshing the page, enhancing the user experience.

3. Setting up a Firebase Project and Exploring the Firebase Console

Now, let’s move from theory to practice and set up a Firebase project to understand these concepts in action.

3.1. Creating a Firebase Project

To begin, navigate to firebase.com in your web browser.

  • Sign up or Log in: If you don’t have a Firebase account, you will need to sign up for a free account. If you already have an account, log in and proceed to the console.

  • Accessing the Console: Once logged in, click the “Go to console” link. This will take you to the Firebase console, your central hub for managing Firebase projects.

  • Adding a New Project: If you are new to Firebase, you might see a blank page. Look for the “Add project” button and click it to create a new Firebase project.

    Firebase project: A container in Firebase that represents a specific application and its associated Firebase services. Each project has its own Firebase Console and configuration.

  • Project Naming and Configuration:

    • Give your project a descriptive name. For example, “Net Ninja Game Guides”.
    • You can optionally change the project location.
    • Accept the Firebase terms and conditions.
    • Click “Create project”.
  • Accessing Project Console: After project creation, click the “Continue” button. This will redirect you to the Firebase console specifically for your newly created project.

3.2. Exploring the Firebase Console and Enabling Authentication

The Firebase console provides access to all the services and features available for your project.

  • Service Overview: You will see a dashboard with various Firebase features like Authentication, Firestore Database, Realtime Database, Cloud Functions, and more.

  • Navigating to Authentication: To set up authentication, click on “Authentication” in the left-hand menu.

  • Setting up Sign-in Methods: By default, Authentication might not be enabled for your project. Click on the “Sign-in method” tab.

  • Enabling Email/Password Authentication: You will see a list of available sign-in methods, such as Email/Password, Google, Facebook, and more. For this course, we will focus on Email and password authentication.

    Sign-in method: The method used to authenticate users, such as email/password, Google Sign-in, Facebook Login, etc. Firebase supports various sign-in methods.

    Email and password authentication: A common sign-in method where users authenticate using their email address and a password. Firebase provides built-in support for this.

    • Click on “Email/Password” and enable it by toggling the switch to “Enabled”.
    • Click “Save”.
  • User Management: After enabling Email/Password authentication, navigate to the “Users” tab. Here, you can view a list of users who have signed up for your application. You can also manually add users from this console, although in a real application, users will typically sign up through the frontend.

3.3. Setting up Firestore Database

Firebase offers two database options: Realtime Database and Firestore. We will be using Firestore, the newer and recommended option for most use cases.

Realtime Database: Firebase’s original NoSQL database, offering real-time data synchronization. While still functional, Firestore is the recommended database for new projects.

  • Navigating to Database: In the Firebase console, click on “Firestore Database” in the left-hand menu.

  • Creating a Database: Click on “Create database”.

  • Security Rules Configuration: You will be prompted to choose security rules. For development purposes, select “Start in test mode”.

    Test mode: A Firestore security rules setting that allows unrestricted read and write access to the database for development purposes. It is less secure and should not be used in production.

    • Understanding Test Mode: Test mode allows unrestricted read and write access to your database, simplifying development initially. However, it is crucial to understand that this mode is insecure and should not be used in production environments.

    • Locked Mode: The alternative, locked mode, enforces security rules from the start, requiring authentication for database access.

    Locked mode: A Firestore security rules setting that restricts database access based on defined rules, typically requiring authentication. This provides better security for production environments.

    • For this initial setup, “Start in test mode” is recommended for ease of development. We will revisit security rules later in the course.
    • Click “Next” and choose a location for your Firestore database, then click “Enable”.
  • Firestore Interface: Once the database is created, you will see the Firestore interface. It will initially be empty. You can see options to “Add collection”. Collections in Firestore are used to organize your data. We will be creating collections like “guides” and “users” later on, primarily from the frontend of our application.

    Collections: In Firestore, collections are groups of documents, similar to tables in relational databases. They are used to organize and structure data within a Firestore database.

Security rules: Rules defined in Firebase to control access to Firebase services, such as Firestore and Cloud Storage. They determine who can read, write, and perform other operations based on authentication and data conditions.

4. Next Steps

With our Firebase project set up, authentication enabled, and Firestore database initialized, we are now ready to move to the frontend development. In the next steps, we will:

  • Create the HTML template for our application.
  • Start interacting with Firebase from the frontend to implement user signup and login functionalities.
  • Begin working with Firestore to store and retrieve data.

This foundational setup provides the necessary backend infrastructure to build a fully functional application with Firebase authentication and database integration.


Setting Up the Frontend Template for Firebase Authentication

Introduction

This chapter guides you through the initial steps of setting up the frontend template for a web application that will utilize Firebase Authentication. We will focus on creating the basic HTML structure, incorporating styling with Materialize CSS, and implementing interactive elements using JavaScript. This template will serve as the foundation for integrating Firebase Authentication in subsequent chapters. We will be using a speed-coding approach, primarily copying and adapting pre-written code to quickly establish the template. While the focus is on setting up the structure for authentication, understanding the underlying HTML, CSS, and JavaScript principles is beneficial. Resources for learning more about these technologies, particularly Materialize CSS, will be provided.

Project Setup and File Structure

To begin, we need to organize our project files. Create a new folder named firebase-auth. Inside this folder, create an image folder to store images used in the website, and a scripts folder for JavaScript files.

Within the firebase-auth folder, create the following files:

  • index.html: This will be the main HTML file for our website, containing the structure and content.
  • scripts/off.js: This file will contain JavaScript code specifically related to Firebase authentication logic.
  • scripts/index.js: This file will contain JavaScript code for DOM manipulation and general frontend interactivity.

The image folder should contain a logo image named logo.svg. This logo will be used in the website’s navigation bar. You can obtain this image from the provided GitHub repository, specifically from the lesson 3 branch, within the image folder.

HTML Boilerplate and Materialize CSS Integration

Open index.html and create the basic HTML document structure. You can use an HTML boilerplate generator (like Emmet’s !tab shortcut in many code editors) to quickly create this structure. The title of the document, which appears in the browser tab, should be set to “Game Guides”.

Next, we need to integrate Materialize CSS into our project. Materialize CSS is a responsive front-end framework based on Material Design principles, providing pre-built CSS classes and components to style our website quickly.

Materialize CSS: A modern responsive front-end framework based on Material Design. It provides pre-designed CSS classes and JavaScript components to quickly style and add functionality to web applications.

Add the following <link> tag within the <head> section of your index.html file to include Materialize CSS from a Content Delivery Network (CDN). Alternatively, you can download Materialize CSS and link to it locally.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">

CDN (Content Delivery Network): A geographically distributed network of servers that cache static content (like CSS, JavaScript, and images) and deliver it to users based on their location, resulting in faster loading times.

Building the Navigation Bar

The navigation bar (navbar) is a crucial element for website navigation. We will use Materialize CSS classes to style and structure our navbar. Within the <body> section of index.html, add the following HTML code to create the navbar:

<body class="grey lighten-3">

  <nav class="z-depth-0 grey lighten-4">
    <div class="nav-wrapper container">
      <a href="#" class="brand-logo">
        <img src="image/logo.svg" alt="Game Guides Logo" style="width: 180px; margin-top: 10px;">
      </a>
      <ul id="nav-mobile" class="right hide-on-med-and-down">
        <li class="logged-in modal-trigger" data-target="modal-account"><a href="#">Account</a></li>
        <li class="logged-in"><a href="#">Logout</a></li>
        <li class="logged-in modal-trigger" data-target="modal-create"><a href="#">Create Guide</a></li>
        <li class="logged-out modal-trigger" data-target="modal-login"><a href="#">Login</a></li>
        <li class="logged-out modal-trigger" data-target="modal-signup"><a href="#">Sign Up</a></li>
      </ul>
    </div>
  </nav>

  <!-- Modals will be added here -->

  <div class="container" style="margin-top: 20px;">
    <!-- Collapsible content will be added here -->
  </div>

  <!-- JavaScript files will be linked here -->

</body>

Let’s break down the navbar code:

  • <body class="grey lighten-3">: This applies Materialize CSS classes to the <body> element, setting a light grey background.
  • <nav class="z-depth-0 grey lighten-4">: This is the <nav> element, which is the container for the navbar.
    • z-depth-0: Materialize CSS class to remove the default shadow from the navbar.
    • grey lighten-4: Materialize CSS class to set the navbar background to a lighter shade of grey.
  • <div class="nav-wrapper container">: This <div> acts as a wrapper for the navbar content.
    • nav-wrapper: Materialize CSS class required for navbar structure.
    • container: Materialize CSS class to center the content within the navbar and limit its width on larger screens.

container (Materialize CSS): A Materialize CSS class that centers content horizontally on the page and applies a maximum width, ensuring readability on larger screens.

  • <a href="#" class="brand-logo">: This is an anchor tag (<a>) that represents the website logo.
    • brand-logo: Materialize CSS class for styling the logo in the navbar.
    • <img src="image/logo.svg" ...>: This <img> tag displays the logo.svg image. Inline styles are used to set the width and top margin of the logo.
  • <ul id="nav-mobile" class="right hide-on-med-and-down">: This unordered list (<ul>) contains the navigation menu items.
    • id="nav-mobile": An ID to potentially target this list in JavaScript if needed.
    • right: Materialize CSS class to align the menu items to the right side of the navbar.
    • hide-on-med-and-down: Materialize CSS class to hide these menu items on medium-sized screens and smaller (for responsive design, often used with a mobile menu).
  • <li class="logged-in modal-trigger" data-target="modal-account">...</li>: Each list item (<li>) represents a menu item.
    • logged-in and logged-out: Custom classes used to control the visibility of menu items based on user login status (will be implemented later).
    • modal-trigger: Materialize CSS class that designates this menu item as a trigger to open a modal.
    • data-target="modal-account": A data attribute that specifies the ID of the modal to be opened when this menu item is clicked.

anchor tag (<a>): An HTML element used to create hyperlinks to other web pages, files, or locations within the same page. In this context, they are used as clickable menu items, and the href="#" attribute makes them act as links without navigating to a new page.

<ul> and <li>: HTML elements used to create unordered lists and list items, respectively. <ul> is the container for the list, and <li> elements represent individual items within the list.

class (HTML/CSS): An attribute in HTML used to group elements together for styling and manipulation using CSS and JavaScript. Materialize CSS classes provide pre-defined styles and behaviors.

Z-depth (Materialize CSS): A Materialize CSS property that controls the visual depth or elevation of elements, often represented by shadows. A z-depth-0 class removes the shadow.

Creating Modals for Authentication and Actions

Modals are pop-up windows that appear on top of the main content, commonly used for login forms, signup forms, and displaying information. We will create modals for signup, login, account details, and creating a guide.

modal: A dialog box or pop-up window that appears on top of the current page content. It is often used to display important information, forms, or require user interaction without navigating away from the current page.

Add the following modal structures within the <body> section, below the <nav> element:

  </nav>

  <!-- Sign Up Modal -->
  <div id="modal-signup" class="modal">
    <div class="modal-content">
      <h4>Sign Up</h4>
      <form id="signup-form">
        <div class="input-field">
          <label for="signup-email">Email</label>
          <input type="email" id="signup-email" required>
        </div>
        <div class="input-field">
          <label for="signup-password">Password</label>
          <input type="password" id="signup-password" required>
        </div>
        <button class="btn yellow darken-3 z-depth-0">Sign Up</button>
      </form>
    </div>
  </div>

  <!-- Login Modal -->
  <div id="modal-login" class="modal">
    <div class="modal-content">
      <h4>Login</h4>
      <form id="login-form">
        <div class="input-field">
          <label for="login-email">Email</label>
          <input type="email" id="login-email" required>
        </div>
        <div class="input-field">
          <label for="login-password">Password</label>
          <input type="password" id="login-password" required>
        </div>
        <button class="btn yellow darken-3 z-depth-0">Login</button>
      </form>
    </div>
  </div>

  <!-- Account Modal -->
  <div id="modal-account" class="modal">
    <div class="modal-content center-align">
      <h4>Account Details</h4>
      <div class="account-details"></div>
    </div>
  </div>

  <!-- Create Guide Modal -->
  <div id="modal-create" class="modal">
    <div class="modal-content">
      <h4>Create New Guide</h4>
      <form id="create-form">
        <div class="input-field">
          <label for="title">Title</label>
          <input type="text" id="title" required>
        </div>
        <div class="input-field">
          <label for="content">Content</label>
          <textarea id="content" class="materialize-textarea" required></textarea>
        </div>
        <button class="btn yellow darken-3 z-depth-0">Create</button>
      </form>
    </div>
  </div>

  <div class="container" style="margin-top: 20px;">
    <!-- Collapsible content will be added here -->
  </div>

  <!-- JavaScript files will be linked here -->

</body>

Each modal structure follows a similar pattern:

  • <div id="modal-signup" class="modal">: The outer <div> element defines the modal container.
    • id="modal-signup": A unique ID to reference this modal, matching the data-target attribute in the navbar.
    • class="modal": Materialize CSS class that identifies this <div> as a modal and applies default modal styling.
  • <div class="modal-content">: This <div> contains the content of the modal.
    • modal-content: Materialize CSS class for styling the modal content area.
  • <h4>Sign Up</h4>: A heading for the modal.
  • <form id="signup-form">: A <form> element to contain input fields and a submit button.
    • id="signup-form": An ID to target this form in JavaScript for form submission and validation.
  • <div class="input-field">: Materialize CSS class for styling input fields and labels.
  • <label for="signup-email">Email</label>: A <label> element to describe the input field.
  • <input type="email" id="signup-email" required>: An <input> element for email input.
    • type="email": Specifies the input type as email, providing basic email validation.
    • id="signup-email": An ID to associate with the <label> and target in JavaScript.
    • required: HTML attribute to make this input field mandatory.
  • <input type="password" id="signup-password" required>: An <input> element for password input.
    • type="password": Masks the input characters for password security.
  • <textarea id="content" class="materialize-textarea" required></textarea>: A <textarea> element for multi-line text input, used in the “Create Guide” modal for guide content.
    • class="materialize-textarea": Materialize CSS class to style the <textarea> to work with Materialize’s input field styling.
  • <button class="btn yellow darken-3 z-depth-0">Sign Up</button>: A <button> element to submit the form.
    • btn: Materialize CSS class for styling buttons.
    • yellow darken-3: Materialize CSS classes to style the button with a dark yellow color.
    • z-depth-0: Materialize CSS class to remove the shadow from the button.

<div>: A generic HTML container element used to group other HTML elements together. It is often used for structuring and styling web pages.

form: An HTML element (<form>) used to create interactive forms for collecting user input. Forms typically contain input fields, labels, and submit buttons.

input field: An area within a form where users can enter data. HTML provides various types of input fields, such as text fields (<input type="text">), email fields (<input type="email">), and password fields (<input type="password">).

label: An HTML element (<label>) used to provide a descriptive caption for form input fields or other form elements, improving accessibility and user experience.

textarea: An HTML element (<textarea>) used to create a multi-line text input area, suitable for larger amounts of text input like descriptions or content.

Adding Collapsible Content for Game Guides

To display game guides, we will use Materialize CSS’s collapsible component. This will allow us to display titles of guides, and expand them to show the content when clicked.

collapsible (Materialize CSS): A Materialize CSS component that creates vertically stacked expandable sections of content. Each section has a header that can be clicked to toggle the visibility of its content body.

Add the following collapsible structure within the <div class="container" style="margin-top: 20px;"> element:

  <div class="container" style="margin-top: 20px;">
    <ul class="collapsible">
      <li>
        <div class="collapsible-header grey lighten-4"><i class="material-icons">chevron_right</i>Guide Title 1</div>
        <div class="collapsible-body white"><span>Guide content goes here. Lorem ipsum dolor sit amet.</span></div>
      </li>
      <li>
        <div class="collapsible-header grey lighten-4"><i class="material-icons">chevron_right</i>Guide Title 2</div>
        <div class="collapsible-body white"><span>More guide content. Consectetur adipiscing elit.</span></div>
      </li>
      <li>
        <div class="collapsible-header grey lighten-4"><i class="material-icons">chevron_right</i>Guide Title 3</div>
        <div class="collapsible-body white"><span>Even more guide content. Sed do eiusmod tempor incididunt.</span></div>
      </li>
    </ul>
  </div>

  <!-- JavaScript files will be linked here -->

</body>

Let’s examine the collapsible structure:

  • <ul class="collapsible">: The outer <ul> element defines the collapsible container.
    • class="collapsible": Materialize CSS class that identifies this <ul> as a collapsible list and applies the necessary styling and JavaScript functionality (which we will initialize later).
  • <li>: Each <li> element represents a collapsible item.
  • <div class="collapsible-header grey lighten-4">: This <div> is the header of the collapsible item, which is visible and clickable.
    • collapsible-header: Materialize CSS class for styling the header of a collapsible item.
    • grey lighten-4: Materialize CSS class to set the header background to a lighter shade of grey.
    • <i class="material-icons">chevron_right</i>: An icon (using Material Icons, which are included with Materialize CSS) to visually indicate the expand/collapse state.
  • <div class="collapsible-body white">: This <div> is the body of the collapsible item, which is initially hidden and becomes visible when the header is clicked.
    • collapsible-body: Materialize CSS class for styling the body of a collapsible item.
    • white: Materialize CSS class to set the body background to white.
    • <span>Guide content goes here...</span>: Placeholder content for the guide.

Initializing Materialize Components with JavaScript

Materialize CSS components, like modals and collapsibles, often require JavaScript initialization to function correctly. We need to include the Materialize JavaScript library and our custom JavaScript files in index.html.

Add the following <script> tags just before the closing </body> tag:

  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
  <script src="scripts/index.js"></script>
  <script src="scripts/off.js"></script>

</body>
  • <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>: Includes the Materialize JavaScript library from a CDN. This is necessary for Materialize components to work.
  • <script src="scripts/index.js"></script>: Includes our custom JavaScript file index.js, which will contain code for initializing Materialize components and handling DOM manipulation.
  • <script src="scripts/off.js"></script>: Includes our custom JavaScript file off.js, which will contain code related to Firebase authentication (we will add content to this file in later chapters).

Now, create the scripts folder and the files index.js and off.js as outlined in the “Project Setup and File Structure” section.

Open scripts/index.js and add the following JavaScript code to initialize the modals and collapsible elements:

document.addEventListener('DOMContentLoaded', function() {
  var modals = document.querySelectorAll('.modal');
  M.Modal.init(modals);

  var items = document.querySelectorAll('.collapsible');
  M.Collapsible.init(items);
});

Let’s break down this JavaScript code:

  • document.addEventListener('DOMContentLoaded', function() { ... });: This code ensures that the JavaScript code inside the function runs only after the entire HTML document has been fully loaded and parsed by the browser. This is important because we need the HTML elements to exist in the Document Object Model (DOM) before we can manipulate them with JavaScript.

DOM (Document Object Model): A programming interface for HTML and XML documents. It represents the structure of a document as a tree of objects, where each object represents a part of the document (e.g., elements, attributes, text). JavaScript uses the DOM to access and manipulate HTML elements dynamically.

JavaScript: A programming language primarily used for front-end web development to add interactivity, dynamic content, and complex functionality to websites.

event listener: A function in JavaScript that waits for a specific event to occur (e.g., DOMContentLoaded, click, mouseover) and then executes a predefined block of code when that event happens.

  • var modals = document.querySelectorAll('.modal');: This line selects all HTML elements in the document that have the class modal. document.querySelectorAll() is a JavaScript method that returns a NodeList (a collection) of elements matching the CSS selector provided (in this case, .modal).

querySelectorAll: A JavaScript method of the document object that returns a static NodeList representing a list of the document’s elements that match the specified CSS selector.

  • M.Modal.init(modals);: This line initializes the Materialize CSS modal functionality for the selected modal elements. M is the global Materialize object, Modal is the Modal component within Materialize, and init() is a method of the Modal component that initializes it. We pass the modals NodeList to init() to initialize all modal elements found on the page.

method (programming): A function that is associated with an object. In this case, init() is a method of the M.Modal object. Methods perform operations on the object’s data or properties.

initialize (programming/frameworks): To set up and prepare a component or library for use. Initialization often involves setting up event listeners, configuring default settings, and preparing the component for user interaction.

  • var items = document.querySelectorAll('.collapsible');: This line selects all HTML elements with the class collapsible, similar to how we selected modals.
  • M.Collapsible.init(items);: This line initializes the Materialize CSS collapsible functionality for the selected collapsible elements, similar to modal initialization.

Save all files. Now, if you open index.html in a web browser (using a live server extension is recommended for automatic refreshing during development), you should see the basic website template with a navbar, modals that can be triggered by clicking the menu items, and collapsible guide sections.

Conclusion

In this chapter, we have successfully created the basic frontend template for our Firebase Authentication project. We set up the project structure, integrated Materialize CSS for styling and components, built a navigation bar with modal triggers, created modals for authentication and actions, and added collapsible sections for displaying game guides. We also initialized the Materialize CSS components using JavaScript.

This template provides a solid foundation for the next steps, where we will focus on integrating Firebase Authentication to handle user signup, login, logout, and account management. The structure we have built will allow us to easily incorporate these authentication features and build a fully functional web application.


Connecting Your Front-End Website to Firebase: A Step-by-Step Guide

This chapter will guide you through the process of connecting a front-end website, developed locally, to a Firebase backend. We will cover the essential steps to initialize Firebase in your project and set up references to key Firebase services, specifically Authentication and Cloud Firestore.

1. Introduction to Firebase Integration

In modern web development, it’s common to separate the front-end and back-end of an application.

Front-end: The user interface and client-side logic of a website or application that users directly interact with in their web browser. It is responsible for displaying information and handling user input.

Back-end: The server-side logic, databases, and infrastructure that power a website or application. It handles data storage, processing, and security, often hidden from direct user interaction.

In this context, we’ve already created the basic structure (template) of our website using HTML.

HTML (HyperText Markup Language): The standard markup language for creating web pages and web applications. It provides the structure and content of a webpage.

Now, we aim to connect this HTML-based front-end to Firebase, a comprehensive platform for mobile and web development.

Firebase: A Backend-as-a-Service (BaaS) platform provided by Google that offers various tools and services for building, managing, and growing apps, including databases, authentication, hosting, and more.

This connection will enable us to utilize Firebase’s powerful backend services within our website.

2. Obtaining the Firebase Configuration Snippet

To begin, navigate to your project in the Firebase console.

Firebase console: A web interface provided by Firebase where you can manage your Firebase projects, configure services, view analytics, and access various Firebase features.

Within the Firebase console, go to the Project overview.

Project overview: The main dashboard of your Firebase project in the Firebase console, providing a summary of your project’s services and settings.

Look for the button to add Firebase to your web app (often represented by a </> icon or labeled “Web”). Clicking this button will generate a code snippet.

Code snippet: A small block of pre-written code provided by Firebase that contains the necessary configuration details to connect your web application to your specific Firebase project.

This snippet is crucial for establishing the connection between your front-end and your Firebase backend.

3. Integrating the Configuration Snippet into Your HTML

The generated code snippet needs to be embedded into your HTML file. Locate the </body> tag at the bottom of your HTML document. Paste the entire snippet just before the closing </body> tag, ensuring it is placed before any other <script> tags you might have in your HTML.

This placement is important because it ensures that the Firebase configuration is loaded before any scripts that depend on Firebase services.

4. Optimizing Firebase Library Imports: Modular Approach

Initially, the code snippet includes a <script> tag that references the entire Firebase SDK (Software Development Kit).

Firebase SDK (Software Development Kit): A set of software development tools and libraries provided by Firebase that allows developers to integrate Firebase services into their applications.

Firebase library: A collection of code modules that provide access to Firebase functionalities. The SDK is composed of these libraries.

Loading the entire Firebase library can be inefficient if you only intend to use specific features. To optimize performance, we will adopt a modular approach by importing only the necessary Firebase modules.

Replace the original <script> tag in the snippet with the following modular imports:

<script src="https://www.gstatic.com/firebasejs/9.VERSION.firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.VERSION.firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.VERSION.firebase-firestore.js"></script>

Replace 9.VERSION with the actual version number provided in your Firebase snippet.

These individual script tags import:

  • firebase-app.js: This imports the core functionality of Firebase, essential for initializing and using any Firebase service.

    Core functionality: The fundamental and essential features of Firebase that are required for basic operation and for using other Firebase services.

  • firebase-auth.js: This imports the authentication module, allowing us to use Firebase Authentication services for user management.

    Authentication: The process of verifying the identity of a user or device. Firebase Authentication provides tools to manage user sign-up, sign-in, and access control.

  • firebase-firestore.js: This imports the Cloud Firestore module, enabling us to use Firebase’s NoSQL cloud database.

    Cloud Firestore: A flexible, scalable NoSQL cloud database to store and sync data for client- and server-side development. It is part of the Firebase suite of services.

This modular import approach ensures that we only load the code necessary for the features we intend to use (Authentication and Cloud Firestore), reducing the overall size of the loaded resources and improving website performance.

Modular imports: A method of importing only specific parts or modules of a library or SDK, rather than the entire library. This can improve efficiency and reduce the size of the code loaded.

5. Understanding the Firebase Configuration Object

Below the script tags in the snippet, you’ll find a JavaScript object containing your Firebase project’s configuration.

Object: In JavaScript, an object is a collection of key-value pairs, used to represent data and structure information. Here, it holds configuration settings for Firebase.

Configuration: The set of parameters and settings that define how a system or application is set up and operates. In this context, it’s the Firebase project’s specific details.

This configuration object typically includes:

  • apiKey: This is your project’s API key.

    API key (Application Programming Interface key): A unique identifier that authenticates requests associated with your Firebase project. It allows your application to access Firebase services.

    The API key acts as an identifier, allowing your application to connect to the correct Firebase project.

    Identifier: A name or value that uniquely identifies something. In this case, the API key uniquely identifies your Firebase project.

    While it’s included in client-side code, Firebase security rules are essential for securing your database and ensuring data protection.

    Security rules: Rules defined in Firebase to control access to your Firebase services, such as Cloud Firestore and Storage. They specify who can read, write, and perform other operations on your data, enhancing security.

  • authDomain: Specifies your Firebase project’s authentication domain.

  • projectId: Your unique Firebase project ID.

  • storageBucket: (Optional) Specifies the Firebase Storage bucket for your project. In this tutorial, we are not using Firebase Storage, so this line can be safely deleted from the configuration object.

    Storage bucket: A storage location in Firebase Storage where you can store files like images, videos, and documents.

  • messagingSenderId: (Optional) Used for Firebase Cloud Messaging. This line can also be deleted as we are not using this feature.

  • appId: A unique identifier for your Firebase application.

It is crucial to understand that while the API key is publicly visible in your front-end code, Firebase security rules are the primary mechanism for securing your backend data.

6. Initializing the Firebase Application

The final line in the snippet uses the firebase.initializeApp(config) method.

initializeApp(): A Firebase function used to initialize the Firebase application in your project, using the provided configuration object.

This line of code initializes the Firebase application using the configuration object we discussed. This initialization establishes the connection between your front-end website and your Firebase backend. After this step, you can start using various Firebase services in your application.

7. Creating References to Firebase Services: Authentication and Firestore

To utilize specific Firebase services like Authentication and Cloud Firestore, we need to create references to them within our JavaScript code. We will use constants to store these references for easy access throughout our application.

Constant: A variable whose value cannot be changed after it is initially assigned. In JavaScript, const is used to declare constants.

const auth = firebase.auth();
const db = firebase.firestore();
  • const auth = firebase.auth();: This line initializes the auth service and stores a reference to it in the auth constant.

    Auth service: Firebase Authentication’s service for managing user authentication, including user sign-up, sign-in, and session management.

    Now, you can use the auth constant to access various methods provided by the Firebase Authentication service, such as user sign-up (createUserWithEmailAndPassword), sign-in (signInWithEmailAndPassword), and sign-out (signOut).

    Methods: Functions that are associated with an object or a class. In this context, methods of the auth and db objects allow you to interact with Firebase Authentication and Firestore services.

  • const db = firebase.firestore();: Similarly, this line initializes the firestore service and stores a reference in the db constant.

    Firestore service: Firebase Cloud Firestore’s service for interacting with the NoSQL database, allowing you to read, write, update, and delete data.

    The db constant allows you to interact with Cloud Firestore, enabling operations like reading and writing documents, querying data, and setting up real-time listeners.

8. Addressing Firestore Settings for Version 5 and Above

For Firebase version 5 and later, it’s necessary to update Firestore settings to prevent potential console errors related to timestamp handling. Add the following code after initializing Firestore:

db.settings({
    timestampsInSnapshots: true
});

This code updates the Firestore settings, specifically the timestampsInSnapshots setting, and sets it to true.

Firestore settings: Configuration options that control how Cloud Firestore behaves, including settings related to timestamps, persistence, and more.

Timestamps in snapshots: A Firestore setting that dictates how timestamps are handled when retrieving data snapshots from the database. Setting it to true ensures consistent timestamp behavior across different Firebase SDK versions.

This adjustment ensures that timestamps are handled correctly and prevents potential errors in your browser’s developer console.

9. Conclusion

By completing these steps, your front-end website is now successfully configured to communicate with your Firebase backend. You have initialized the Firebase application, imported necessary modules, and created references to the Authentication and Cloud Firestore services. You are now ready to start implementing user authentication and database interactions in your web application, which will be covered in subsequent chapters.


User Signup Implementation with Firebase Authentication

This chapter will guide you through the process of implementing user signup functionality in a web application using Firebase Authentication. Building upon the Firebase setup covered in the previous chapter, we will now enable users to create accounts and become authenticated members of our application. This chapter focuses on the front-end implementation using JavaScript to interact with the Firebase backend.

Setting the Stage: Front-end to Back-end Communication

In the preceding chapter, we configured Firebase for our project, establishing a communication channel between the front-end of our application and the back-end managed within the Firebase Console.

Front-end: The part of a web application that users directly interact with, typically built using HTML, CSS, and JavaScript, running in the user’s web browser.

Back-end: The server-side part of a web application, responsible for data storage, processing, and logic, often interacting with databases and APIs.

Firebase Console: A web interface provided by Firebase for managing your Firebase projects, including authentication, database, and other services.

This setup allows us to perform operations such as user signup directly from our application’s user interface.

Implementing User Signup Functionality

Our objective in this chapter is to enable users to sign up for our application. The user interface (UI) for signup involves a “Sign Up” link that triggers a modal window.

Modal: A dialog box or window that appears on top of the current page content, temporarily interrupting the user’s workflow to focus on a specific task or information.

This modal contains fields for the user to enter their email address and password. Upon clicking the “Sign Up” button within the modal, the application will initiate the signup process using the Firebase SDK.

Attaching an Event Listener to the Signup Form

To capture the user’s interaction with the signup form, we need to attach an event listener.

Event listener: A function in JavaScript that waits for a specific event to occur on an HTML element (like a button click or form submission) and then executes a predefined code block.

This listener will be associated with the signup form in our HTML structure.

  1. Accessing the Signup Form in the DOM:

    We first need to obtain a reference to the signup form element in the DOM (Document Object Model).

    DOM (Document Object Model): A programming interface for web documents. It represents the page so that programs can change the document structure, style, and content.

    By inspecting the HTML structure, we identify the signup form element and note its ID: signup-form. We also note the modal’s ID: modal-signup.

    const signupForm = document.querySelector('#signup-form');

    Here, document.querySelector() is used as a query selector to retrieve the HTML element with the ID signup-form.

    Query Selector: A JavaScript method used to select HTML elements from the DOM based on CSS-style selectors (like IDs, classes, or tag names).

  2. Adding a Submit Event Listener:

    Instead of listening for a button click, we will listen for the form submit event.

    Form Submit Event: An event triggered in HTML forms when a user attempts to submit the form data, usually by clicking a submit button or pressing Enter in a form field.

    This event is triggered when the user clicks the “Sign Up” button within the form or presses Enter while focused on a form field.

    signupForm.addEventListener('submit', (event) => {
        // Signup logic will be placed here
    });

    We use signupForm.addEventListener() to attach a listener to the submit event. The second argument is a callback function that will be executed when the submit event occurs.

    Callback Function: A function passed as an argument to another function, to be executed at a later point in time, often after an asynchronous operation completes.

Preventing Default Form Submission

By default, submitting an HTML form triggers a page refresh. We need to prevent this default behavior to handle the signup process using JavaScript without page reloads.

  1. Using preventDefault():

    Inside the event listener callback function, we use the event.preventDefault() method.

    Prevent Default: A method in JavaScript used to stop the default action associated with an event, such as preventing a form from submitting and refreshing the page.

    signupForm.addEventListener('submit', (event) => {
        event.preventDefault(); // Prevent default form submission
        // ... rest of signup logic
    });

    This line of code ensures that the default page refresh is prevented when the form is submitted.

Retrieving User Input: Email and Password

To sign up a user, we need to retrieve the email and password entered by the user in the input fields within the signup form.

Input Fields: HTML elements (like <input>, <textarea>) in a form that allow users to enter data.

  1. Accessing Input Field Values:

    We can access the values of the input fields using square bracket notation and the .value property.

    Square Bracket Notation: A way to access properties of JavaScript objects or elements in arrays using square brackets and a string or index, often used to dynamically access form elements by their IDs or names.

    Value Property: A property of HTML form elements (like input fields) in JavaScript that holds the current value entered by the user in that field.

    The input fields for email and password have IDs signup-email and signup-password respectively.

    const email = signupForm['signup-email'].value;
    const password = signupForm['signup-password'].value;

    This code retrieves the values from the input fields with the specified IDs and stores them in the email and password constants.

  2. Verifying Input (Optional):

    For debugging and verification purposes, we can use console.log() to display the retrieved email and password in the browser’s developer console.

    Console.log: A JavaScript function used for displaying output (like variables or messages) in the browser’s developer console, useful for debugging and logging information.

    console.log("Email:", email);
    console.log("Password:", password);

Signing Up the User with Firebase Authentication

With the user’s email and password retrieved, we can now use the Firebase SDK to create a new user account.

Firebase SDK (Software Development Kit): A set of software tools and libraries provided by Firebase to enable developers to integrate Firebase services into their applications.

  1. Using createUserWithEmailAndPassword():

    The Firebase Authentication SDK provides the createUserWithEmailAndPassword() method to create new user accounts. This method is accessed through the auth object which we initialized in a previous step (referenced in the transcript as being set up in a previous video).

    **createUserWithEmailAndPassword**: A method within the Firebase Authentication SDK used to create a new user account with an email address and password.

    Auth Object: In the context of Firebase, this refers to the Firebase Authentication service object, providing methods for user authentication and management.

    auth.createUserWithEmailAndPassword(email, password)
        .then((cred) => {
            console.log("User created:", cred.user);
            // Handle successful signup
        })
        .catch((error) => {
            console.error("Signup error:", error.message);
            // Handle signup errors
        });

    This code calls createUserWithEmailAndPassword() with the email and password. This is an asynchronous task.

    Asynchronous Task: An operation in programming that does not execute immediately and sequentially but runs in the background, allowing other code to execute while waiting for the task to complete.

    Because it is asynchronous, it returns a Promise.

    Promise: In JavaScript, an object representing the eventual completion (or failure) of an asynchronous operation and allowing you to handle the result when it becomes available.

  2. Handling the Promise with .then() and .catch():

    We use the .then() method to handle the successful completion of the signup process and .catch() to handle any errors that might occur.

    `.then() method: A method used with Promises in JavaScript to specify a callback function that should be executed when the Promise is resolved (i.e., the asynchronous operation is successful).

    • .then() callback: This function is executed if the signup is successful. It receives a user credential object as an argument (named cred in the example).

      User Credential: An object returned by Firebase Authentication methods (like createUserWithEmailAndPassword) containing information about the newly created or signed-in user, including a user token.

      We can log the user object from the credential to the console.

      User Object: Part of the User Credential in Firebase, containing detailed information about the authenticated user, such as email, UID, and other user-specific data.

    • .catch() callback: This function is executed if the signup fails. It receives an error object as an argument. We can log the error message to the console for debugging.

Verifying User Creation in Firebase Console

Upon successful signup, the new user account will be visible in the “Authentication” section of the Firebase Console. Each user is assigned a unique User UID (User ID).

User UID (User ID): A unique identifier automatically generated by Firebase for each user account created in a Firebase project.

This UID is crucial for identifying and managing users within our Firebase project.

Post-Signup Actions: Closing Modal and Resetting Form

After successfully signing up a user, we should improve the user experience by:

  1. Closing the Signup Modal:

    We use the Materialize library (a front-end framework used in this example) to programmatically close the signup modal.

    Materialize Library: A front-end framework based on Material Design principles, providing pre-built CSS and JavaScript components for web development.

    const modal = document.querySelector('#modal-signup');
    const modalInstance = M.Modal.getInstance(modal);
    modalInstance.close();
    • M.Modal.getInstance(modal): This gets the Materialize modal instance associated with the HTML element.

      **m.Modal.getInstance()**: A method in the Materialize JavaScript library used to get the instance of a modal element, allowing programmatic control of the modal (like opening or closing).

    • modalInstance.close(): This method closes the modal window.

      `.close() method: A method of a Materialize Modal instance in JavaScript, used to programmatically close the modal window.

  2. Resetting the Signup Form:

    To clear the email and password fields in the form after signup, we use the .reset() method on the form element.

    `.reset() method: A standard JavaScript method for HTML forms, used to reset all form fields to their initial values.

    signupForm.reset();

    This ensures that the form is ready for the next user interaction.

Conclusion

By following these steps, we have successfully implemented user signup functionality in our web application using Firebase Authentication. This includes capturing user input from a modal form, using the Firebase SDK to create new user accounts, handling asynchronous operations with Promises, and improving the user experience by closing the modal and resetting the form after successful signup. This chapter provides a foundational understanding of client-side authentication implementation with Firebase, setting the stage for more advanced authentication features and user management in subsequent development.


User Logout Functionality in Web Applications Using Firebase Authentication

This chapter will guide you through the process of implementing user logout functionality in a web application using Firebase Authentication. We will build upon the user signup functionality discussed previously and focus on enabling users to securely log out of the application.

Introduction

In the previous steps, we implemented user signup functionality, allowing new users to register and create accounts within our application. Upon successful signup, Firebase automatically logs the user into the application, providing them with an authentication token. However, a complete user authentication system requires not only signup but also the ability for users to securely log out when they are finished using the application. This chapter will detail the implementation of this logout functionality.

Understanding the Logout Process

When a user successfully signs up using Firebase, they are automatically logged in. Firebase provides an auth token upon successful login, which signifies that the user is authenticated and can access protected resources within the application.

Auth Token: A security credential that is issued by an authentication server (like Firebase Authentication) to a client application after successful authentication. This token is used to verify the user’s identity for subsequent requests to the server, eliminating the need to re-enter credentials repeatedly.

To log a user out, we need to utilize a specific Firebase method designed for this purpose. This process is distinct from the signup process and involves invoking a different function provided by the Firebase Authentication service.

Implementing Logout Functionality

To implement the logout feature, we will follow these steps:

  1. Accessing the Logout Link: First, we need to identify the logout link within our application’s navigation bar. In our HTML structure, this link has been assigned the ID logout. We will use JavaScript to access this element.

  2. Attaching an Event Listener: We need to make the logout link interactive. To do this, we will attach a click event listener to the logout link. This listener will execute a function when the user clicks on the link.

    Event Listener: A procedure in JavaScript (and other programming languages) that waits for a specific event to occur (like a user clicking a button or a key press) and then executes a predefined function in response to that event.

  3. Preventing Default Link Behavior: When a user clicks on a link, the default browser behavior is to navigate to the URL specified in the href attribute. In our case, we want to prevent this default behavior and instead execute our logout logic. We can achieve this by using the preventDefault() method on the event object within our event listener function.

    Event Object: An object that is automatically passed to event handler functions in JavaScript. It contains information about the event that occurred, such as the type of event, the target element, and other relevant details.

  4. Using the Firebase signOut() Method: To actually log the user out, we will use the signOut() method provided by Firebase Authentication. This method communicates with the Firebase backend to invalidate the user’s current session.

    Method: In programming, a method is a function that is associated with an object. It defines the actions that can be performed on or by that object. In this context, signOut() is a function associated with the Firebase Authentication object.

  5. Handling the Asynchronous Operation: The signOut() method is an asynchronous task. This means it takes some time to complete as it involves communication with the Firebase server. Asynchronous operations in JavaScript often return promises, which represent the eventual result of an asynchronous operation.

    Asynchronous Task: A task that does not block the main thread of execution in a program. It allows other parts of the program to continue running while the task is being processed in the background. Examples include network requests and file operations.

    Promise: An object in JavaScript that represents the eventual outcome of an asynchronous operation. It can be in one of three states: pending, fulfilled (successful), or rejected (failed). Promises provide a structured way to handle asynchronous operations and their results.

  6. Using the then() Method: Since signOut() returns a promise, we can use the .then() method to execute a callback function after the logout operation is successfully completed. In this example, we will log a message to the console confirming that the user has signed out.

    Callback Function: A function that is passed as an argument to another function and is executed later, typically after an asynchronous operation completes or when a certain event occurs.

  7. Code Implementation:

    // Get a reference to the logout link element using its ID
    const logout = document.querySelector('#logout');
    
    // Add a click event listener to the logout link
    logout.addEventListener('click', (e) => {
      // Prevent the default link behavior
      e.preventDefault();
    
      // Use the Firebase auth object to call the signOut method
      auth.signOut().then(() => {
        // Log a message to the console upon successful logout
        console.log('user signed out');
      });
    });

    In this code snippet:

    • document.querySelector('#logout') selects the HTML element with the ID logout.

    • logout.addEventListener('click', ...) attaches a click event listener to this element.

    • e.preventDefault() prevents the default action of the link.

    • auth.signOut() calls the Firebase signOut() method using the auth object, which is assumed to be a previously initialized Firebase Authentication object.

      Auth Object: In the context of Firebase, the auth object typically refers to the Firebase Authentication service instance. It provides methods for user authentication actions such as signup, login, logout, and managing user accounts.

    • .then(() => { ... }) executes the callback function after the signOut() promise resolves successfully.

    • console.log('user signed out') logs a confirmation message to the browser’s console.

Testing the Logout Functionality

After implementing the logout code, it’s crucial to test it in the browser.

  1. Initial State: At this point, if you have recently signed up, you will likely be logged in due to state persistence.

    State Persistence: The ability of a system to retain its state (data) even after it is restarted or refreshed. In web applications with Firebase Authentication, state persistence often refers to the browser remembering the user’s login session even after the page is closed or refreshed, until the user explicitly logs out.

  2. Triggering Logout: Click on the “logout” link in the navigation bar.

  3. Verification: Observe the console. You should see the message “user signed out” logged to the console, confirming that the logout process was successful.

While there might not be immediate visual changes in the front-end of the application at this stage, the underlying authentication state has been updated. The user is now logged out of the application as far as Firebase is concerned.

Conclusion and Next Steps

In this chapter, we successfully implemented the user logout functionality using Firebase Authentication. This provides a crucial component of a complete authentication flow, allowing users to securely end their sessions.

While the current implementation only logs a message to the console upon logout, in a real-world application, you would typically update the user interface to reflect the logged-out state. This might involve hiding content that requires authentication and redirecting the user to a login page or displaying a different view for unauthenticated users.

In the next chapter, we will focus on implementing the user login functionality, allowing existing users to log back into the application after they have logged out. This will complete the basic login/logout authentication flow for our application.


Chapter: Implementing User Login Functionality

Introduction

Following the implementation of user signup and logout functionalities, the next crucial step in building a comprehensive authentication system is enabling users to log back into the application. This chapter will guide you through the process of implementing user login using email and password authentication, leveraging the Firebase Authentication service. We will build upon the existing codebase to create a seamless login experience for returning users.

Setting up the Login Form and Event Listener

To begin, we need to interact with the login form element within our HTML structure. This involves obtaining a reference to the form and setting up an event listener to capture user login attempts.

Getting a Reference to the Login Form

First, we identify the login form in our HTML document using its unique identifier (ID). In this case, the login form has the ID login-form. We use JavaScript’s document.querySelector() method to select this form and store it in a constant variable for easy access.

// Get a reference to the login form
const loginForm = document.querySelector('#login-form');

document.querySelector() This is a JavaScript method that allows you to select the first element that matches a specified CSS-style selector in the document. It’s a versatile tool for accessing HTML elements using IDs, classes, or tag names.

Adding a Submit Event Listener

Next, we attach an event listener to the loginForm constant. This listener is configured to trigger a function when the form’s submit event occurs. The submit event is automatically fired when a user attempts to submit the form, typically by clicking a submit button or pressing Enter within a form field.

// Add an event listener to the login form
loginForm.addEventListener('submit', (event) => {
  // Event handler code will go here
});

Event Listener In JavaScript, an event listener is a procedure that waits for a specific event to occur on an HTML element. When the event happens, the listener executes a predefined function (callback function) to respond to that event. Common events include clicks, form submissions, and key presses.

Submit Event A submit event is triggered on a <form> element when the form is submitted. This typically happens when a user clicks a submit button or presses Enter while focused on a form field. By default, submitting a form will cause the page to reload or navigate to a new page, depending on the form’s configuration.

Preventing Default Form Submission Behavior

Inside the event listener’s callback function, the first crucial step is to prevent the default form submission behavior. By default, when a form is submitted, the browser attempts to navigate to a new page. We want to handle the login process using JavaScript without page reload. We achieve this using the event.preventDefault() method.

loginForm.addEventListener('submit', (event) => {
  event.preventDefault(); // Prevent default form submission
  // ... rest of the login process code
});

event.preventDefault() This is a method called on an event object in JavaScript. It stops the default action of the event from happening. For example, in the case of a form submission, it prevents the browser from navigating to a new page or reloading the current page.

Retrieving User Login Credentials from Input Fields

To log in a user, we need to collect their login credentials, specifically their email address and password, from the login form’s input fields.

Accessing Email and Password Input Fields

Within the login form, we have input fields for the user’s email and password. We access these input fields using the form element and their respective IDs: login-email and login-password. Similar to how we accessed the form itself, we can use bracket notation on the loginForm constant to select elements within the form by their IDs. We then access the .value property of these input elements to retrieve the text entered by the user.

// Get user info
const email = loginForm['login-email'].value;
const password = loginForm['login-password'].value;

Input Fields Input fields are HTML form elements that allow users to enter data, such as text, numbers, or passwords. They are typically created using the <input> tag with different type attributes, like text, email, password, etc.

Bracket Notation In JavaScript, bracket notation is a way to access properties of an object using a string or variable inside square brackets []. In this context, it is used to access form elements by their IDs as if the form was an object and the IDs were its properties. For example, loginForm['login-email'] is equivalent to accessing a property named ‘login-email’ on the loginForm object.

Variable Scope in JavaScript Functions

It’s important to note that when we declare variables (using const in this case) within a function’s scope, they are locally scoped to that function. This means that even if we use the same variable names (like email and password) in different functions, they are treated as distinct variables and do not conflict with each other. This is because each function creates its own isolated scope.

Scope (in Programming) In programming, scope refers to the region of a program where a defined variable can be accessed or modified. JavaScript has function scope, meaning variables declared within a function are only accessible within that function. This helps in organizing code and preventing naming conflicts between different parts of a program.

Implementing User Login with Firebase Authentication

With the user’s email and password retrieved, we can now utilize Firebase Authentication to attempt to log the user in.

Using the signInWithEmailAndPassword Method

Firebase Authentication provides a method called signInWithEmailAndPassword specifically for logging in users using their email and password credentials. This method is accessed through the auth object, which is our reference to the Firebase Authentication service. We call this method, passing in the retrieved email and password as arguments.

// Sign in the user
auth.signInWithEmailAndPassword(email, password)
  .then((cred) => {
    // Handle successful login
  })
  .catch((err) => {
    // Handle login errors
    console.log(err.message);
  });

Method (in Programming) In object-oriented programming (and JavaScript, which is prototype-based), a method is a function that is associated with an object. Methods perform operations on the object or related to the object. In this context, signInWithEmailAndPassword is a method provided by the Firebase auth object to handle user login.

Auth Object (Firebase Authentication) The auth object in Firebase Authentication is the primary interface for interacting with the authentication service. It provides methods for user signup, login, logout, managing user profiles, and more. It is typically initialized using the Firebase SDK.

signInWithEmailAndPassword This is a method provided by Firebase Authentication’s auth object. It is used to sign in a user with their email address and password. It takes the email and password as arguments and returns a Promise that resolves with user credentials upon successful login, or rejects with an error if login fails.

Asynchronous Operations and Promises

The signInWithEmailAndPassword method, like other Firebase Authentication methods for signup and logout, is asynchronous. This means it doesn’t execute immediately and block the rest of the code. Instead, it initiates the login process and returns a Promise.

Asynchronous (Programming) Asynchronous operations are non-blocking operations. In JavaScript, asynchronous operations, like network requests or Firebase calls, are initiated and run in the background without halting the execution of the main program thread. When the operation completes, it triggers a callback or resolves a Promise to handle the result.

Promise (JavaScript) A Promise is a JavaScript object representing the eventual outcome of an asynchronous operation. It can be in one of three states: pending (initial state, neither fulfilled nor rejected), fulfilled (operation completed successfully), or rejected (operation failed). Promises provide a structured way to handle asynchronous operations and their results, including success and failure scenarios.

Handling the Promise with .then() and .catch()

We use the .then() method to specify a callback function that will be executed if the login is successful (the Promise resolves). This function receives a credential object as an argument, which contains information about the logged-in user.

If the login fails (e.g., incorrect email or password), the Promise will be rejected. We use the .catch() method to provide a callback function that will be executed in case of an error. This function typically receives an error object containing details about why the login failed.

auth.signInWithEmailAndPassword(email, password)
  .then((cred) => {
    // Handle successful login
    console.log(cred.user); // Log user info from credential
  })
  .catch((err) => {
    // Handle login errors
    console.log(err.message);
  });

.then() Method (Promise) The .then() method is used to attach a callback function to a Promise that will be executed when the Promise is fulfilled (i.e., the asynchronous operation is successful). It can also be chained to handle sequential asynchronous operations.

.catch() Method (Promise) The .catch() method is used to attach a callback function to a Promise that will be executed when the Promise is rejected (i.e., the asynchronous operation fails). It is used for error handling in asynchronous operations.

Accessing User Information from the Credential Object

Upon successful login, the .then() callback function receives a credential object. This object contains vital information about the authentication process and the logged-in user. Crucially, the credential.user property provides access to the user object, which contains user details such as their UID, email, and other profile information.

Credential Token (Authentication) In the context of authentication, a credential token is a piece of data that represents a user’s identity and authorization. It’s issued after successful authentication and can be used to verify the user’s identity in subsequent requests. In Firebase Authentication, the credential object returned after successful login contains such tokens and user information.

User Property (Firebase Authentication Credential) The user property within the Firebase Authentication credential object provides access to a User object. This User object contains detailed information about the authenticated user, including their unique ID (UID), email address, display name, and other profile data.

Post-Login Actions: Closing Modal and Resetting Form

After a successful login, we want to provide a clean user experience. This typically involves closing the login modal window and resetting the login form to clear the input fields for future use.

Closing the Login Modal

Similar to how we closed the signup modal after successful signup, we need to close the login modal after successful login. We first obtain a reference to the login modal element using document.querySelector(), specifying the ID of the login modal (in this case, assuming it’s modal-login). We then use a method from a UI library (like Materialize CSS, implied by the transcript) to get an instance of the modal and close it.

// Close the login modal
const modalLogin = document.querySelector('#modal-login');
const modalInstanceLogin = M.Modal.getInstance(modalLogin);
modalInstanceLogin.close();

Modal (User Interface) A modal, also known as a modal window or dialog box, is a UI element that appears on top of the current application window. It creates a mode that disables the main application window until the modal is closed. Modals are often used for displaying important messages, forms, or options that require user interaction before proceeding.

getInstance (Materialize CSS) getInstance is a method provided by the Materialize CSS JavaScript library for modals (and other components). It is used to get the JavaScript instance of a Materialize CSS component that has been initialized on a DOM element. This instance provides methods to control the component, such as opening and closing a modal.

Resetting the Login Form

To clear the input fields in the login form after successful login, we can use the .reset() method on the loginForm element. This method is a standard HTML form method that clears all input fields within the form to their default values.

// Reset the login form
loginForm.reset();

.reset() (Form Method) .reset() is a built-in method for HTML form elements. When called on a form element, it resets all the form controls (input fields, textareas, selects, etc.) to their initial values as defined in the HTML or as set by JavaScript.

Testing the User Login Functionality

To ensure the login functionality is working correctly, we can test it using a user account that has already been created through the signup process. By entering the registered email and password into the login form and submitting it, we should observe the following:

  • The login modal closes.
  • User information, retrieved from the Firebase Authentication service, is logged to the browser’s console. This confirms successful authentication and retrieval of user data.
  • Subsequent application logic, which might be dependent on user login status, should now be able to recognize the logged-in user.

Conclusion

This chapter has detailed the process of implementing user login functionality using Firebase Authentication with email and password. We covered setting up the login form, retrieving user credentials, using the signInWithEmailAndPassword method, handling the asynchronous nature of the login process with Promises, and performing post-login actions like closing the modal and resetting the form. With signup, logout, and now login implemented, we have established the core authentication functionalities for our application. The next step will be to explore how to track the user’s authentication status within the application to dynamically adapt the user interface and application behavior based on whether a user is logged in or not. This will be crucial for building personalized user experiences and implementing features that require authentication.


Understanding User Authentication Status in Web Applications

This chapter explores how to track the authentication status of users in web applications. Building upon previous discussions of user signup, login, and logout functionalities, we will now focus on maintaining awareness of whether a user is currently logged in or logged out. This is crucial for tailoring the user experience and displaying appropriate content based on their authentication state.

Why Track Authentication Status?

In most web applications, the content and features available to a user depend on their authentication status.

  • Logged-out users: Typically see public content, signup/login prompts, and general information.
  • Logged-in users: Gain access to personalized content, user-specific features, and account settings.

Therefore, it is essential to have a mechanism to continuously monitor and react to changes in a user’s authentication status. This allows the application to dynamically adjust the user interface and functionality based on whether they are currently authenticated.

Utilizing onAuthStateChanged for Real-time Status Updates

To effectively track authentication status, we employ a method called onAuthStateChanged. This method acts as a listener, constantly monitoring for any changes in a user’s authentication state within the application.

Authentication Status: Refers to the current state of a user’s session within an application, specifically whether they are currently logged in (authenticated) or logged out (not authenticated).

When an authentication status change occurs (e.g., a user logs in or logs out), onAuthStateChanged triggers a predefined function, known as a callback function. This function is then executed, allowing the application to respond immediately to the change in authentication status.

Callback Function: In programming, a callback function is a function that is passed as an argument to another function. It is executed when a specific event occurs or when the outer function completes its task. In this context, the callback function is executed whenever the authentication state changes.

Setting up the onAuthStateChanged Listener

To implement this listener, we typically initialize it within the application’s authentication module or service. The following steps outline the setup process:

  1. Access the Authentication Object: First, you need to access the authentication object provided by your chosen authentication service (e.g., Firebase Authentication). This object usually provides methods for managing user authentication, including onAuthStateChanged.

  2. Call onAuthStateChanged: Invoke the onAuthStateChanged method on the authentication object. This method accepts a single argument: a callback function.

    auth.onAuthStateChanged(function(user) {
      // Callback function code will be executed here
    });
  3. Define the Callback Function: The callback function receives a user object as a parameter. This user object contains information about the currently authenticated user, if one exists.

    • User Logged In: If a user has successfully logged in, the user parameter will be an object containing details about that user (e.g., user ID, email).

    • User Logged Out: If a user has logged out, or if no user is currently logged in, the user parameter will be null.

    Null: In programming, null represents the intentional absence of a value or object. In this context, a null user object signifies that no user is currently authenticated.

Responding to Authentication State Changes within the Callback Function

Inside the callback function, you can implement logic to handle different authentication states. A common approach is to check if the user object is present (i.e., not null).

  • User is Logged In (user is not null):

    if (user) {
      console.log('User logged in:', user);
      // Perform actions for logged-in users, e.g., display user-specific content
    }

    In this case, the code within the if block will execute, indicating that a user has logged in. You can then perform actions such as:

    • Displaying user-specific content.
    • Navigating the user to their dashboard.
    • Updating the user interface to reflect the logged-in state.
  • User is Logged Out (user is null):

    else {
      console.log('User logged out');
      // Perform actions for logged-out users, e.g., display login prompts
    }

    If the user object is null, the code within the else block will execute, indicating that the user is logged out. Actions to perform in this scenario might include:

    • Displaying login or signup prompts.
    • Hiding user-specific content.
    • Redirecting to a public landing page.

Initial Authentication Check on Application Load

It’s important to note that onAuthStateChanged is not only triggered by explicit login or logout actions. It also fires when the application initially loads. This allows the application to immediately determine the user’s authentication status upon startup.

Upon initial load, the authentication service checks for any existing user sessions.

  • Session Exists: If a user is already logged in (due to a persistent session), onAuthStateChanged will fire, and the callback function will receive the authenticated user object.
  • No Session Exists: If no user is logged in, onAuthStateChanged will still fire, but the callback function will receive null for the user object.

This initial check ensures that the application’s user interface and functionality are correctly initialized based on the user’s authentication status from the moment they access the application.

Example Scenario: Displaying Content Based on Authentication Status

Imagine an application with “guides” that are only accessible to logged-in users. Using onAuthStateChanged, you can dynamically control the visibility of these guides:

auth.onAuthStateChanged(function(user) {
  const guidesSection = document.getElementById('guides-section'); // Assume you have a section with ID 'guides-section'

  if (user) {
    console.log('User logged in:', user);
    guidesSection.style.display = 'block'; // Show guides for logged-in users
  } else {
    console.log('User logged out');
    guidesSection.style.display = 'none';  // Hide guides for logged-out users
  }
});

In this example, the guides-section will be visible only when a user is logged in, providing a tailored experience based on their authentication status.

Conclusion

The onAuthStateChanged method is a fundamental tool for managing user authentication status in web applications. By setting up a listener and implementing appropriate logic within the callback function, applications can react dynamically to changes in authentication state, providing a seamless and personalized user experience. This ensures that users see the correct content and have access to the appropriate features based on whether they are currently logged in or out.


Working with Firestore Database to Dynamically Display Content

This chapter explores how to integrate a Firestore database with a web application to dynamically display content based on data stored in the database. We will move away from hard-coded content and learn how to retrieve and display information from Firestore, a flexible and scalable NoSQL cloud database.

Introduction to Dynamic Content Display

In the previous steps, we successfully implemented user authentication, allowing us to track user login and logout status. We can now leverage this authentication status to control content visibility. However, before implementing content restriction based on login status, we will address the issue of hard-coded content within our application.

Currently, the guides displayed on our page are directly written into the HTML template.

Template: In web development, a template refers to a pre-designed structure or format that can be dynamically populated with data to generate the final output, such as a webpage or document.

<!-- Example of hard-coded guides (as described in the transcript) -->
<li>
    <div class="collapsible-header grey lighten-3">Guide Title 1</div>
    <div class="collapsible-body"><span>Guide Content 1</span></div>
</li>
<li>
    <div class="collapsible-header grey lighten-3">Guide Title 2</div>
    <div class="collapsible-body"><span>Guide Content 2</span></div>
</li>

Hard-coded: Refers to data or values that are directly embedded into the source code of a program, rather than being obtained from external sources or user input.

This approach is not ideal for managing content that needs to be updated or expanded. To make our application more dynamic and maintainable, we will transition to storing guide data in a Firestore database.

Introduction to Firestore

Firestore is a flexible, scalable database from Firebase, designed for mobile, web, and server development. It is a NoSQL document database, which means data is stored in documents grouped into collections, rather than in traditional tables and rows.

Firestore: A NoSQL document database offered by Firebase. It is designed for scalability and flexibility, allowing developers to store and sync data for web and mobile applications in real-time.

NoSQL: A type of database that does not adhere to the traditional relational database structure. NoSQL databases are often schema-less, distributed, and designed for scalability and performance.

Collection: In Firestore, a collection is a grouping of documents. It’s analogous to a table in relational databases, but without a fixed schema.

Document: In Firestore, a document is a lightweight record that contains fields, which map to values. Documents are the units of data storage within a collection and are similar to rows in relational databases or JSON objects.

Firestore offers several advantages for our application, including:

  • Real-time updates: Data changes can be synchronized across connected clients in real-time.
  • Scalability: Firestore is designed to handle large amounts of data and users.
  • Flexibility: The NoSQL document model allows for storing diverse and evolving data structures.

While a comprehensive exploration of Firestore is beyond the scope of this chapter, Firebase provides extensive documentation and resources for in-depth learning. For those new to Firestore, supplementary materials, such as dedicated video playlists, are highly recommended.

Setting up a Firestore Database and Collection

Assuming a Firebase project and Firestore database have already been initialized (as covered in earlier materials), we can proceed to structure our guide data within Firestore.

  1. Accessing Firestore Console: Navigate to the Firebase console for your project and select “Firestore Database” from the left-hand menu.

  2. Creating a Collection: If no collections exist, you will be prompted to create one. Click “Create Database” if necessary, and then “Start in production mode” for a secure setup. Once the database is ready, click “Add collection”.

  3. Naming the Collection: We will name our collection “guides” to store information about our application guides. Click “Next”.

  4. Creating the First Document: Firestore organizes data into collections of documents. Each guide will be represented as a document within the “guides” collection.

    • Document ID: Firestore can automatically generate unique IDs for each document. Leave the “Document ID” field set to “Auto-ID”. This simplifies document creation as we don’t need to manually generate unique identifiers.

      Document ID: A unique identifier for each document within a Firestore collection. Firestore can automatically generate these IDs, ensuring uniqueness within the collection.

    • Document Fields: Documents store data in fields, which are key-value pairs. For our guides, we will define two fields:

      • title (String): The title of the guide. Example: “Find all the stars in Mario 64”.
      • content (String): The content or body of the guide. Example: “Lorem ipsum…” (placeholder text).
  5. Saving the Document: Click “Save” to create the first document in the “guides” collection.

  6. Adding More Documents: To add more guides, click “Add document” within the “guides” collection and repeat step 4 and 5, creating new documents for each guide. For example, a second guide could have the title “Beat Rainbow Road in record time”.

After completing these steps, you should see the “guides” collection in your Firestore database, containing documents representing each guide with their respective titles and content. Firestore automatically handles the unique Document IDs, simplifying data management.

Retrieving Data from Firestore in the Frontend

Now that we have data stored in Firestore, we need to retrieve and display it in our web application’s frontend. We will use JavaScript code to connect to Firestore and fetch the guide data.

  1. Database Reference: In our JavaScript code (specifically within auth.js as per the transcript), we need to establish a reference to our Firestore database. This is typically done during the Firebase initialization process. If you have followed previous setup steps, you should already have a db constant initialized.

    // Assuming Firebase is initialized in index.js or a similar setup file
    // const firebaseConfig = { ... }; // Your Firebase configuration
    // firebase.initializeApp(firebaseConfig);
    
    // In auth.js (or where you handle Firestore interactions)
    const db = firebase.firestore(); // Initialize Firestore

    Reference (to database): In programming, a reference is a value that enables a program to access data indirectly, often pointing to the location in memory where the data is stored. In this context, db is a reference to the Firestore database instance.

  2. Accessing the Collection: To retrieve guides, we need to specify the “guides” collection. We use the collection() method on our db reference.

    db.collection('guides')
  3. Retrieving Documents: To fetch all documents within the “guides” collection, we use the get() method. This method initiates an asynchronous operation to retrieve data from Firestore.

    db.collection('guides').get()

    Asynchronous task: A process in programming that operates independently of the main program flow. Asynchronous tasks do not block the execution of other operations while they are running, and they typically notify the program when they are completed. Retrieving data from a database over a network is an asynchronous task.

    Promise: In JavaScript, a promise is an object representing the eventual result of an asynchronous operation. It can be in one of three states: pending, fulfilled (with a value), or rejected (with a reason). Promises provide a structured way to handle asynchronous operations and their outcomes.

  4. Handling the Promise: The get() method returns a Promise because retrieving data from Firestore is an asynchronous operation. We use the .then() method to handle the successful completion of the data retrieval.

    db.collection('guides').get().then((snapshot) => {
        // Code to process the retrieved data (snapshot) will go here
    });

    Callback function: A function passed as an argument to another function, which is expected to be executed (or “called back”) at a later point in time, often after an asynchronous operation completes. In the .then() method of a Promise, the function provided is a callback that executes when the Promise is resolved (successful).

  5. Snapshot and Document Access: When the Promise resolves successfully, the .then() callback function receives a snapshot. This snapshot represents the state of the “guides” collection at the time of the query. To access the individual documents within the collection, we use snapshot.docs.

    Snapshot (of Firestore collection): A snapshot in Firestore represents the data of a collection or document at a specific point in time. It’s essentially a read-only, in-memory copy of the data retrieved from the database.

    db.collection('guides').get().then((snapshot) => {
        console.log(snapshot.docs); // Logs an array of DocumentSnapshot objects
    });

    snapshot.docs is an array containing DocumentSnapshot objects, each representing a document in the “guides” collection. At this point, logging snapshot.docs to the console will display these document objects.

Displaying Firestore Data in the DOM

Now that we can retrieve the guide data from Firestore in our JavaScript code, the next step is to dynamically display this data in our web application’s Document Object Model (DOM). We will modify our index.js file to handle the DOM manipulation.

DOM (Document Object Model): A programming interface for HTML and XML documents. It represents the structure of the document as a tree of objects, where each object represents a part of the document (e.g., elements, attributes, text). JavaScript can use the DOM to dynamically access and update the content, structure, and style of web pages.

  1. Getting a Reference to the Guides List Element: In index.js, we need to get a reference to the HTML element where we want to display the guides. Based on the transcript and HTML structure, this is likely an unordered list (<ul>) element with the class “guides”.

    // index.js
    const guideList = document.querySelector('.guides');
    
    > **`querySelector()`:** A JavaScript method of the `document` object that selects the first HTML element within the document that matches a specified CSS selector. In this case, it selects the first element with the class 'guides'.
  2. Creating a setupGuides Function: We will create a function called setupGuides in index.js to handle the process of taking the retrieved guide data and rendering it to the DOM. This function will accept the array of document snapshots (snapshot.docs) as input.

    // index.js
    const guideList = document.querySelector('.guides');
    
    const setupGuides = (data) => {
        let html = ''; // Initialize an empty string to build HTML
        // Logic to process 'data' and build HTML will go here
    };
  3. Calling setupGuides from auth.js: In auth.js, after retrieving the snapshot data in the .then() callback, we will call the setupGuides function and pass snapshot.docs as the argument.

    // auth.js
    db.collection('guides').get().then((snapshot) => {
        setupGuides(snapshot.docs); // Pass the document snapshots to setupGuides
    });
  4. Iterating Through Documents and Accessing Data: Inside the setupGuides function, we need to iterate through the data array (which is snapshot.docs) and extract the data from each document. We use the forEach() method to loop through the array and the .data() method to access the fields of each document.

    const setupGuides = (data) => {
        let html = '';
        data.forEach(doc => {
            const guide = doc.data(); // Get the document data
            console.log(guide); // Logs the title and content of each guide
            // HTML template creation logic will go here
        });
    };

    .data() (on Firestore document): A method available on a Firestore DocumentSnapshot object. It returns a JavaScript object containing the data fields and values stored in the Firestore document.

    forEach(): A JavaScript array method that executes a provided function once for each element in an array. It’s used for iterating over array elements and performing operations on them.

  5. Creating an HTML Template for Each Guide: For each guide document, we will create an HTML string representing the guide’s display structure. We will use template literals (backticks `) for easy string interpolation and multi-line strings. This HTML structure will mirror the original hard-coded guide structure, using <li>, <div> elements, and relevant CSS classes.

    const setupGuides = (data) => {
        let html = '';
        data.forEach(doc => {
            const guide = doc.data();
            const li = `
                <li>
                    <div class="collapsible-header grey lighten-3">${guide.title}</div>
                    <div class="collapsible-body"><span>${guide.content}</span></div>
                </li>
            `;
            html += li; // Append the current guide's HTML to the overall HTML string
        });
        // Setting innerHTML logic will go here
    };

    Template string (backticks): A feature in JavaScript that allows for string interpolation (embedding variables within strings) and multi-line strings. Template strings are enclosed in backticks (`) instead of single or double quotes. Placeholders within template strings, denoted by ${expression}, are replaced with the values of the expressions.

    li tag (HTML): Represents a list item. It is used within ordered lists (<ol>) and unordered lists (<ul>) to define individual items in the list.

    ul tag (HTML): Represents an unordered list. It is used to group a set of related items where the order does not matter. List items within a <ul> are typically displayed with bullet points.

    div tag (HTML): Stands for “division”. It is a generic container element used to group and structure other HTML elements. <div> elements are often used for styling and layout purposes.

    class (HTML attribute): An HTML attribute used to assign one or more class names to an HTML element. Class names are used to apply CSS styles to elements and to select elements using JavaScript.

  6. Setting innerHTML to Display Guides: After iterating through all the documents and building the complete HTML string in the html variable, we need to set the innerHTML property of the guideList element to this HTML string. This will update the DOM and display the dynamically generated guides on the page.

    const setupGuides = (data) => {
        let html = '';
        data.forEach(doc => {
            const guide = doc.data();
            const li = `... (HTML template as before) ...`;
            html += li;
        });
        guideList.innerHTML = html; // Set the innerHTML of the guideList element
    };

    innerHTML: A property of HTML elements in JavaScript that allows you to get or set the HTML content of an element. Setting innerHTML replaces the existing HTML content of the element with the provided HTML string.

  7. Verification: After saving the changes and refreshing the browser, the guides should now be dynamically loaded from the Firestore database and displayed on the page. Inspecting the page source or using browser developer tools will confirm that the guide list is being populated dynamically.

Conclusion and Next Steps

By integrating Firestore and implementing dynamic content display, we have moved away from hard-coded guides and created a more flexible and maintainable application. We successfully retrieved guide data from Firestore and rendered it in the DOM.

The next step is to enhance our application by controlling the visibility of these guides based on user authentication status. This will ensure that only logged-in users can access and view the guide content, adding a layer of security and personalized experience to our application. This will be addressed in the subsequent chapter.


Securing Data Access Based on User Authentication in Web Applications

This chapter explores methods to control data visibility and access in web applications based on user authentication status. We will focus on using Firebase and its services, specifically Firestore and Firebase Authentication, to manage data retrieval and security rules. Building upon the previous discussion on displaying data from a Firestore database, this chapter will demonstrate how to dynamically update the user interface (UI) based on whether a user is logged in or out. Furthermore, we will delve into securing the Firestore database itself using security rules, ensuring that only authenticated users can access sensitive data.

1. Dynamically Updating the User Interface Based on Authentication Status

In many web applications, it’s crucial to display different content or features depending on whether a user is logged in. For instance, you might want to show personalized data or restrict access to certain functionalities for logged-in users only. This section will cover how to modify the UI of our application to reflect the user’s authentication state.

1.1. Listening for Authentication State Changes

Firebase Authentication provides a mechanism to monitor changes in a user’s login status. The onAuthStateChanged listener is a key component in achieving this dynamic UI update.

onAuthStateChanged: This is a method provided by Firebase Authentication that sets up a listener to detect changes in the user’s authentication state. It is triggered whenever a user logs in, logs out, or when the authentication state is initially determined upon page load.

In our application, we utilize this listener to detect when a user logs in or out. Previously, we were simply logging messages to the console upon these state changes. Now, we will enhance this by retrieving data and updating the UI accordingly.

firebase.auth().onAuthStateChanged(user => {
  if (user) {
    console.log('user logged in:', user);
    // Retrieve data and update UI for logged-in users
  } else {
    console.log('user logged out');
    // Update UI for logged-out users
  }
});

1.2. Conditional Data Display Based on Login Status

When a user is logged in, we want to display data, such as guides from our Firestore database. Conversely, when a user is logged out, we should hide this data or display a message prompting them to log in.

To achieve this, we will modify the code within the onAuthStateChanged listener. When a user is logged in, we retrieve the guide data from Firestore and use the setupGuides method to display it on the UI.

firebase.auth().onAuthStateChanged(user => {
  if (user) {
    console.log('user logged in:', user);
    db.collection('guides').get().then(snapshot => {
      setupGuides(snapshot.docs);
    });
  } else {
    console.log('user logged out');
    setupGuides([]); // Pass an empty array to clear the UI
  }
});

UI (User Interface): This refers to the visual part of an application that users interact with, including elements like buttons, text, images, and layout.

When a user logs out, we still call the setupGuides method, but this time we pass an empty array as the data. This will effectively clear the existing guide list from the UI, as the setupGuides function will iterate over an empty array and generate no HTML elements for the guides.

1.3. Enhancing the Logged-Out User Experience

Instead of simply displaying nothing when a user is logged out, we can improve the user experience by showing a message indicating that they need to log in to view the guides. We can achieve this by modifying the setupGuides function.

Currently, the setupGuides function likely iterates through the provided data and generates HTML to display each guide. We can add a conditional check within this function to handle cases where no data is provided (i.e., when the user is logged out).

const guideList = document.querySelector('.guides');
const setupGuides = (data) => {
  let html = '';
  if (data.length) { // Check if there is data
    data.forEach(doc => {
      const guide = doc.data();
      const li = `
        <li>
          <h5>${guide.title}</h5>
          <p>${guide.content}</p>
        </li>
      `;
      html += li;
    });
  } else { // If no data, display a message
    html = `<h5 class="center-align">Login to view guides</h5>`;
  }
  guideList.innerHTML = html;
};

inner HTML: This is a property in JavaScript that allows you to get or set the HTML content within an HTML element. By setting innerHTML, you can dynamically update the content of an element on a web page.

By checking the data.length property, we can determine if we have received guide data (user logged in) or an empty array (user logged out). If data.length is greater than zero, we proceed to generate the guide list as before. Otherwise, we set the innerHTML of the guideList element to a message prompting the user to log in. The center-align class is used for styling purposes to center the message.

data.length: In JavaScript, length is a property of arrays that returns the number of elements in the array. In this context, data.length checks if the data array (passed to setupGuides) contains any elements.

This approach provides a more user-friendly experience for logged-out users, guiding them on how to access the content.

2. Securing Firestore Data with Security Rules

While updating the UI based on authentication provides a better user experience, it is not sufficient for securing sensitive data. A technically savvy user could still potentially access the data by directly interacting with the Firestore database using the application’s configuration and API key, even if the UI hides it. To truly protect our data, we must implement security rules within Firestore itself.

Firestore: Firebase Firestore is a NoSQL document database built for mobile and web application development. It’s designed to store, sync, and query data at scale. API Key: An API (Application Programming Interface) key is a unique identifier used to authenticate requests to an API. In the context of Firebase, it’s used to authorize your application to access Firebase services. Server-side: Refers to operations and logic that are executed on a server, rather than on the client-side (user’s browser). Security rules are enforced server-side, making them a robust security measure.

2.1. Understanding Firestore Security Rules

Firestore Security Rules are a powerful feature that allows you to control access to your database at a granular level. These rules are defined in the Firebase console and are evaluated by Firebase servers whenever a client attempts to read or write data.

Security Rules: These are declarative rules defined for Firebase Firestore that control access to data. They specify who can read and write data to different parts of your database based on various conditions, including authentication state. Firebase: Firebase is a comprehensive mobile and web application development platform provided by Google. It offers a wide range of services, including databases (Firestore, Realtime Database), authentication, cloud functions, hosting, and more. Console (Firebase Console): The Firebase Console is a web-based interface where you can manage your Firebase projects, configure services like Firestore and Authentication, view analytics, and set up security rules.

By default, when you initialize a Firestore database, it often starts in “test mode” with very permissive rules to allow for easy development. However, these default rules are not secure for production applications.

Test Mode: In Firebase Security Rules, test mode refers to a configuration where the initial rules are very open, typically allowing read and write access to anyone for a limited time. This is intended for development and testing but is not secure for production environments.

2.2. Moving from Test Mode to Secure Rules

The transcript highlights that our Firestore database is currently in test mode, indicated by an exclamation mark and a warning message in the Firebase console stating, “Your security rules are defined as public, so anyone can steal, modify, or delete data in your database.” This is because the default security rules are overly permissive, allowing anyone to read and write to any document in the database.

The current default rules might look something like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true; // INSECURE - Allows anyone to read and write
    }
  }
}

This rule set, particularly allow read, write: if true;, grants unrestricted read and write access to all documents in the database. For a production application, we need to replace these permissive rules with more restrictive ones.

2.3. Implementing Security Rules for the Guides Collection

To secure our guide data, we want to restrict access to only authenticated users. We can achieve this by modifying the security rules to specifically target the guides collection and enforce authentication.

First, we comment out or remove the overly general rule that matches all documents (match /{document=**}). Then, we create a new rule that specifically matches documents within the guides collection.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // match /{document=**} {  // Commented out the general rule
    //   allow read, write: if true;
    // }

    match /guides/{guideId} { // Rule specific to the 'guides' collection
      allow read, write: if request.auth != null;
    }
  }
}

Collection: In Firestore, a collection is a group of documents. It’s used to organize your data. Collections can contain documents and other collections (subcollections). Document: In Firestore, a document is a unit of data within a collection. Documents are structured as key-value pairs and can contain various data types, including strings, numbers, booleans, arrays, and objects.

Let’s break down the new rule:

  • match /guides/{guideId}: This line specifies that the following rules apply to any document within the guides collection. {guideId} is a wildcard that matches any document ID within the guides collection.

  • allow read, write: if request.auth != null;: This is the core of the security rule. It states that read and write access is allowed only if the condition request.auth != null is true.

    request.auth: In Firestore Security Rules, request.auth is an object that provides information about the user making the request, if they are authenticated. It is null if the user is not authenticated. Authentication: Authentication is the process of verifying the identity of a user or device. In Firebase, Authentication services manage user sign-up, sign-in, and user identity verification. null: In programming, null is a special value that represents the intentional absence of an object value or variable. In the context of request.auth, null signifies that no user is currently authenticated for the request.

    The condition request.auth != null checks if the request.auth object is not null. This means that the rule will only allow read and write access if a user is authenticated (logged in). If a user is not logged in, request.auth will be null, and the condition will be false, denying read and write access.

match /databases/{database}/documents: This is the root path in Firestore Security Rules, indicating that the rules are being defined for the Firestore database. All subsequent match statements are relative to this path. match /databases/{database}/documents/guides/{guideId}: This line specifies a more specific path within the Firestore database, targeting documents within the “guides” collection. {guideId} is a wildcard that matches any document ID within the “guides” collection. allow read, write: if request.auth != null: This is the core rule statement. allow read, write specifies the actions being controlled (read and write access). if request.auth != null is the condition that must be met for the actions to be allowed.

By deploying these updated security rules in the Firebase console, we effectively restrict access to the guides collection to only authenticated users, significantly enhancing the security of our application’s data.

2.4. Testing Security Rules

After implementing security rules, it’s crucial to test them thoroughly to ensure they are working as intended. You can use the Firebase console’s simulator to test your rules without affecting your live database. You can simulate authenticated and unauthenticated requests to verify that the rules correctly allow or deny access based on authentication status.

3. Summary

This chapter demonstrated two key aspects of securing data access in web applications using Firebase:

  • UI Updates based on Authentication: We learned how to dynamically update the user interface to show or hide data based on the user’s login status using the onAuthStateChanged listener and conditional rendering within the setupGuides function. This improves user experience by providing context-aware content.

  • Firestore Security Rules: We explored how to implement Firestore Security Rules to protect data at the server level. By restricting access to the guides collection to authenticated users using request.auth != null, we ensured that only logged-in users can read and write guide data, regardless of UI manipulations on the front-end.

These techniques are essential for building secure and user-friendly web applications that handle sensitive data and user authentication. The next step, as hinted in the transcript, might involve further enhancing the UI by dynamically updating navigation elements based on the user’s authentication state, providing a more seamless and personalized user experience. This could involve showing different navigation links or menu items to logged-in and logged-out users, further refining the application’s behavior based on authentication status.

Navar (Navbar): A Navbar, or Navigation Bar, is a common UI element in web applications. It’s typically located at the top of a webpage and contains links to different sections or pages of the website, often used for site navigation.

By combining UI-level adjustments with server-side security rules, we can create robust and secure web applications that effectively manage data access based on user authentication.


Dynamic User Interface Manipulation Based on User Login Status

This chapter will guide you through the process of dynamically manipulating the User Interface (UI) of a web application based on the user’s login status. Specifically, we will focus on modifying the navigation bar (navbar) links to display different options depending on whether a user is logged in or logged out.

Currently, the navigation bar in our web application displays all available links regardless of the user’s login status. This is not ideal as some links, such as “Account,” “Log Out,” and “Create Guide,” are only relevant to logged-in users. Conversely, links like “Log In” and “Sign Up” are primarily intended for users who are not yet logged in.

Our objective is to implement a system that dynamically adjusts the displayed navigation links based on the user’s authentication state.

To achieve this dynamic behavior, we will implement the following steps:

  1. Initial State: Hide All Links: We will begin by hiding all navigation links by default using CSS styling. This ensures that no links are visible until our JavaScript code determines the appropriate links to display based on the user’s status.
  2. Identify Logged-In and Logged-Out Links: We will categorize the navigation links into two groups: “logged-in links” and “logged-out links.” This categorization will be based on HTML classes assigned to each link element.
  3. Create a setupUI Function: We will develop a JavaScript function named setupUI that will be responsible for controlling the visibility of these link groups. This function will:
    • Accept the current user object as a parameter.
    • Check if a user object exists (indicating a logged-in state).
    • If a user is logged in, display the “logged-in links” and hide the “logged-out links.”
    • If a user is logged out (no user object exists), display the “logged-out links” and hide the “logged-in links.”
  4. Integrate with Authentication Logic: We will integrate the setupUI function with our authentication system (in this case, Firebase Authentication) to call this function whenever the user’s login status changes. This ensures that the UI is updated dynamically upon login and logout events.

Step-by-Step Implementation

Let’s break down the implementation step by step.

First, we need to examine our index.html file and identify the navigation links. We will then assign CSS classes to categorize them as either “logged-in” or “logged-out” links.

<nav>
    <ul>
        <li class="logged-in"><a href="#">Account</a></li>
        <li class="logged-in"><a href="#">Log Out</a></li>
        <li class="logged-in"><a href="#">Create Guide</a></li>
        <li class="logged-out"><a href="#">Log In</a></li>
        <li class="logged-out"><a href="#">Sign Up</a></li>
    </ul>
</nav>

In this example, we have added the class logged-in to the links intended for logged-in users and the class logged-out to the links for logged-out users.

Next, we need to write JavaScript code to select these link elements based on their assigned classes. We will use document.querySelectorAll() to retrieve collections of elements.

const loggedOutLinks = document.querySelectorAll('.logged-out');
const loggedInLinks = document.querySelectorAll('.logged-in');

document.querySelectorAll() This is a JavaScript method that returns a static (not live) NodeList representing a list of the document’s elements that match the specified group of selectors. In this case, it selects all elements with the classes ‘logged-out’ and ‘logged-in’ respectively.

Here, loggedOutLinks and loggedInLinks are now variables holding collections of HTML elements that have the respective classes.

3. JavaScript: Creating the setupUI Function

Now, we create the setupUI function to manage the visibility of these link collections based on the user’s login status.

const setupUI = (user) => {
    if (user) {
        // User is logged in
        loggedInLinks.forEach(item => item.style.display = 'block');
        loggedOutLinks.forEach(item => item.style.display = 'none');
    } else {
        // User is logged out
        loggedInLinks.forEach(item => item.style.display = 'none');
        loggedOutLinks.forEach(item => item.style.display = 'block');
    }
};

Let’s analyze this function step by step:

  • Function Definition: const setupUI = (user) => { ... } defines a function named setupUI that accepts a single parameter user. This parameter will represent the current user object.

  • Conditional Logic: if (user) { ... } else { ... } checks if the user object exists. In many authentication systems, including Firebase Authentication, a user object is returned when a user is logged in, and null or undefined is returned when logged out. Therefore, this condition effectively checks the user’s login status.

  • Display Manipulation (Logged-in State):

    loggedInLinks.forEach(item => item.style.display = 'block');
    loggedOutLinks.forEach(item => item.style.display = 'none');

    If a user is logged in (if (user) is true), this code iterates through each element in the loggedInLinks collection using forEach. For each item (representing a link element), it sets the style.display property to 'block'.

    style.display = 'block' This is a way to manipulate the CSS display property of an HTML element using JavaScript. Setting it to 'block' makes the element visible and displayed as a block-level element, meaning it will take up the full width available and start on a new line.

    Simultaneously, it iterates through loggedOutLinks and sets their style.display to 'none', effectively hiding them.

    style.display = 'none' Setting the CSS display property to 'none' hides the element completely from the page. It is removed from the normal document flow and takes up no space.

  • Display Manipulation (Logged-out State):

    loggedInLinks.forEach(item => item.style.display = 'none');
    loggedOutLinks.forEach(item => item.style.display = 'block');

    If a user is logged out (else block), this code performs the opposite action. It hides the loggedInLinks and displays the loggedOutLinks.

4. Integrating setupUI with Authentication

Finally, we need to call the setupUI function at appropriate points in our authentication logic to ensure the UI updates correctly when the user logs in or logs out. Assuming we are using Firebase Authentication, we would typically have an authentication state observer. We need to call setupUI within this observer.

// Example integration with Firebase Auth (Conceptual - Adapt to your Firebase setup)
auth.onAuthStateChanged((user) => {
    setupUI(user); // Call setupUI with the current user object
    // ... other authentication state handling logic ...
});

auth.onAuthStateChanged() This is a method provided by Firebase Authentication. It sets up an observer that is triggered whenever the user’s authentication state changes. The callback function passed to onAuthStateChanged receives the current user object as an argument.

In this example, auth.onAuthStateChanged is assumed to be a Firebase Authentication method that listens for changes in user login status. Inside the callback function of onAuthStateChanged, we call setupUI(user), passing the current user object. This ensures that setupUI is executed whenever the authentication state changes, dynamically updating the navigation links.

To prevent the brief flicker of all links being visible on page load, we should initially hide all navigation links using inline CSS styles directly in the index.html file.

<nav>
    <ul>
        <li class="logged-in" style="display: none;"><a href="#">Account</a></li>
        <li class="logged-in" style="display: none;"><a href="#">Log Out</a></li>
        <li class="logged-in" style="display: none;"><a href="#">Create Guide</a></li>
        <li class="logged-out" style="display: none;"><a href="#">Log In</a></li>
        <li class="logged-out" style="display: none;"><a href="#">Sign Up</a></li>
    </ul>
</nav>

By setting style="display: none;" inline for all <li> elements within the navigation, we ensure that all links are initially hidden when the page loads. The setupUI function will then dynamically override these inline styles to display the appropriate links based on the user’s login status.

Conclusion

By following these steps, we have successfully implemented a dynamic UI update mechanism that modifies the navigation links based on the user’s login status. This approach enhances the user experience by presenting relevant options based on their authentication state, making the application more intuitive and user-friendly. This technique of dynamically manipulating the DOM based on application state is a fundamental concept in modern web development.

DOM (Document Object Model) The Document Object Model (DOM) is a programming interface for web documents. It represents the page so that programs can change the document structure, style, and content. The DOM represents the document as nodes and objects; that way, programming languages can interact with the page.


# Building a Guide Creation Feature with Firebase Firestore

This chapter details the process of adding a guide creation feature to a web application using Firebase Firestore. It builds upon existing user authentication functionality, allowing logged-in users to create and submit new guides that are stored in a Firestore database.

## Setting the Stage: User Interface and Data Flow

Previously, we established a user interface (UI) that dynamically adapts based on user login status. We also ensured that relevant data is correctly displayed for both logged-in and logged-out users. Now, we aim to extend this functionality by enabling logged-in users to contribute content by creating new guides.

> **UI (User Interface):** The visual part of an application or website that users interact with. It includes elements like buttons, menus, and text fields.

Currently, the "Create New Guide" form is present in the UI for logged-in users. This form includes fields for "Guide Title" and "Guide Content." However, upon clicking the "Create" button, no action is taken to persist this data to our backend database, Firestore. Our next step is to "hook up" this form to Firestore, enabling the seamless creation of new guide documents within our database.

> **Firestore:** A flexible and scalable NoSQL cloud database for mobile, web, and server development from Firebase. It's designed to store and sync data for client-side and server-side development.

## Preparing the JavaScript File for Firebase Interactions

To manage interactions with Firebase, including both authentication and Firestore database operations, we will utilize a dedicated JavaScript file, `ar.js`. While initially intended primarily for authentication, its scope is expanded to encompass all Firebase-related functionalities, as user authentication is intrinsically linked to who can create guides.  We will start by establishing a reference to the guide creation form within our JavaScript code.

> **JavaScript file (.js):** A text file containing JavaScript code, used to add interactivity and dynamic behavior to websites.

### Referencing the Create Guide Form

To manipulate the "Create New Guide" form programmatically, we first need to obtain a reference to it in our JavaScript code.  By inspecting the HTML structure, we identify that the form element has an `id` attribute set to `create-formID`.

> **ID (HTML):** A unique identifier assigned to an HTML element. IDs are used to target specific elements with CSS or JavaScript.

We can use the `document.querySelector()` method in JavaScript to select this form element based on its ID.  This selection will be placed within our `ar.js` file, specifically under the existing authentication state change method, and we will add a comment for clarity.

```javascript
// ar.js

// ... (previous code) ...

// Create new guide
const createForm = document.querySelector('#create-formID');

document.querySelector() (JavaScript): A method in JavaScript that allows you to select the first element within the document that matches a specified CSS selector. In this case, it selects the HTML element with the ID ‘create-formID’.

Implementing the Form Submission Event Listener

With a reference to the form obtained, we need to attach an event listener to it. This listener will be triggered when a user submits the form, allowing us to intercept the submission and process the guide data. We will listen for the submit event.

Event Listener (JavaScript): A procedure in JavaScript that waits for a specific event to occur (like a button click or form submission) and then executes a function in response to that event.

createForm.addEventListener('submit', (event) => {
  event.preventDefault(); // Prevent default form submission behavior
  // ... (code to handle form submission) ...
});

submit event (Form): An event that is triggered when a form is submitted, typically when a user clicks a submit button or presses Enter within a form field.

event.preventDefault() (JavaScript): A method called on an event object in JavaScript to prevent the browser from performing its default action associated with that event. For form submissions, it stops the page from reloading or navigating to a new page.

The event.preventDefault() method is crucial here. It prevents the default browser behavior of submitting the form, which would typically cause a page reload. We want to handle the form submission entirely through our JavaScript code, sending the data to Firestore without a page refresh.

Interacting with Firestore to Add New Guides

Inside the event listener, our primary goal is to take the title and content entered by the user in the form and create a new document in our guides collection within our Firestore database. We will utilize the db object, which is assumed to be a pre-existing reference to our Firestore database instance (as established in earlier parts of the project).

Database: A structured collection of data that is organized for easy access, management, and updating. In this context, it refers to the Firestore database.

DB object (Firebase context): Likely a JavaScript object that has been initialized to represent a connection to your Firebase project’s Firestore database. This object provides methods for interacting with Firestore.

Accessing the Firestore Collection and Adding a Document

To interact with a specific collection in Firestore, we use the collection() method on our db object, specifying the collection name, which is guides in this case. To add a new document to this collection, we use the add() method.

collection() method (Firebase Firestore): A method in the Firebase Firestore API that returns a CollectionReference, which is a reference to a specific collection in your Firestore database.

add() method (Firebase Firestore): A method in the Firebase Firestore API that adds a new document to a collection with a randomly generated unique ID. It takes an object representing the document data as an argument.

The add() method expects an object as an argument. This object represents the data for the new document we want to create. For our guides, this object will have two properties: title and content.

createForm.addEventListener('submit', (event) => {
  event.preventDefault();

  db.collection('guides').add({
    title: createForm['title'].value,
    content: createForm['content'].value
  });
});

Object (JavaScript): A fundamental data type in JavaScript that represents a collection of key-value pairs. In this case, the object passed to add() represents the data of a Firestore document.

Properties (Object): Key-value pairs within a JavaScript object. In this context, title and content are properties of the object representing the guide document data.

Retrieving Form Input Values

To get the values entered by the user in the “Guide Title” and “Guide Content” input fields, we access the createForm object. We use square bracket notation along with the id of each input field (title and content) to access these elements. The .value property then retrieves the text entered by the user in each field.

Square bracket notation (JavaScript): A way to access properties of JavaScript objects or elements of arrays using square brackets [] and a string or variable representing the property name or index. In this context, createForm['title'] accesses the form element with the name or id ‘title’.

.value (Input field): A property of HTML input elements that holds the current value entered by the user in that input field, represented as a string.

Alternative Dot Notation:

The transcript mentions that for single-word IDs, dot notation (createForm.title.value) could be used as an alternative. However, square bracket notation (createForm['title'].value) is preferred for consistency and to handle cases where IDs contain hyphens or other characters that are not valid in dot notation.

Dot notation (JavaScript): A way to access properties of JavaScript objects using a dot . followed by the property name. It’s a more concise syntax than square bracket notation but is limited to property names that are valid JavaScript identifiers (e.g., single words without hyphens).

Handling Asynchronous Operations with Promises

The add() method in Firestore is an asynchronous operation. This means it takes time to complete as it involves communication with the Firebase server. Asynchronous operations in JavaScript typically return Promises.

Asynchronous method: A method that performs operations in the background, without blocking the main execution thread of the program. This allows the program to remain responsive while waiting for the operation to complete.

Promise (JavaScript): An object in JavaScript that represents the eventual result of an asynchronous operation. It can be in one of three states: pending, fulfilled (success), or rejected (failure).

Promises provide a way to handle the eventual success or failure of asynchronous operations. We can use the .then() method to execute code when the document is successfully added to Firestore.

.then() method (Promise): A method available on Promise objects in JavaScript that allows you to specify a callback function to be executed when the Promise is successfully resolved (fulfilled).

db.collection('guides').add({
  title: createForm['title'].value,
  content: createForm['content'].value
})
.then(() => {
  // Code to execute after successful document addition
  console.log('Guide successfully added!');
});

Callback function: A function that is passed as an argument to another function and is intended to be executed at a later point in time, typically after an asynchronous operation completes.

Clearing the Form and Closing the Modal After Successful Submission

Upon successful addition of a new guide, we want to provide feedback to the user by clearing the form and closing the modal (pop-up window) that contains the form. The transcript mentions reusing existing code for closing modals and resetting forms, adapting it for the “Create Guide” modal and form.

Modal/Modal (UI): A dialog box or pop-up window that appears on top of the main content of a webpage, often used to focus user attention on a specific task or piece of information.

We need to identify the ID of the “Create Guide” modal, which is modal-create, and the form element, createForm. The code snippet below demonstrates how to close the modal and reset the form using a hypothetical Materialize library for UI components.

Materialize: Likely refers to Materialize CSS, a front-end framework based on Material Design principles, offering pre-built CSS and JavaScript components for web development.

get instance method (Materialize - assumed): Based on context, this likely refers to a method in the Materialize library used to get a programmatic instance of a Materialize component (like a modal), allowing you to control it through JavaScript.

db.collection('guides').add({
  title: createForm['title'].value,
  content: createForm['content'].value
})
.then(() => {
  console.log('Guide successfully added!');

  // Close the modal
  const modalCreate = document.querySelector('#modal-create');
  const modalInstance = M.Modal.getInstance(modalCreate); // Assuming 'M' is the Materialize object
  modalInstance.close();

  // Reset the form
  createForm.reset();
});

Reset form: The action of clearing all input fields within a form, typically setting them back to their default or empty values. The form.reset() method in JavaScript achieves this.

Real-time Updates and Data Synchronization

After implementing guide creation, it’s observed that the list of guides on the UI doesn’t immediately update to reflect the newly added guide. A page refresh is currently required to see the changes. This is because we haven’t yet implemented real-time listeners to the database. The transcript indicates that setting up real-time listeners for automatic UI updates will be addressed in a subsequent step.

Real-time listener: A mechanism that establishes a persistent connection to a database and automatically receives updates whenever data in the database changes. This allows applications to reflect data changes in real-time without manual refreshing.

Firebase Security Rules and Data Protection

The transcript highlights the importance of Firebase Security Rules in protecting our database. These rules control who can access and modify data in Firestore, ensuring data integrity and security.

Rules (Firebase Security Rules): A declarative language used in Firebase to define access control and data validation rules for your Firebase resources, such as Firestore databases and Cloud Storage.

Implementing Security Rules for Guides Collection

The example demonstrates setting up rules to ensure that only authenticated users can read or write to the guides collection. This means users must be logged in to create new guides or even view existing ones (depending on the rule configuration).

Authenticated user: A user whose identity has been verified by an authentication system. In Firebase, this typically involves using Firebase Authentication services.

Read/Write permissions (Database): Permissions that control whether a user or role is allowed to read (access and retrieve) data and/or write (create, modify, delete) data in a database.

The security rules are configured in the Firebase console. The example rule structure might look something like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guides/{guideId} {
      allow read, write: if request.auth != null;
    }
  }
}

This rule specifies that for any document within the guides collection (match /guides/{guideId}), both read and write operations are allowed (allow read, write) only if the request is authenticated (if request.auth != null). This effectively restricts guide creation and viewing to logged-in users.

Client-Side Security Bypasses and Server-Side Enforcement

The transcript illustrates a scenario where a technically inclined user (“savvy front-end developer”) might attempt to bypass front-end UI restrictions (like hiding the “Create Guide” link for logged-out users) by manipulating the HTML and CSS using browser developer tools.

Front-end development: The branch of web development that focuses on creating the user interface and user experience of websites and web applications. It involves technologies like HTML, CSS, and JavaScript that run in the user’s web browser.

Inspect element (Browser DevTools): A feature available in web browsers’ developer tools (DevTools) that allows users to examine and modify the HTML structure, CSS styles, and JavaScript code of a webpage in real-time.

Nav wrapper (HTML - Assumed context): Likely refers to an HTML element, possibly a <div> or <nav> tag, that acts as a container or wrapper for the navigation elements of a webpage.

Display property (CSS): A CSS property that controls the visibility and rendering box type of an HTML element. It can be set to values like block, none, inline, etc.

Block display (CSS): A value for the CSS display property that makes an element behave as a block-level element, meaning it takes up the full width available and starts on a new line.

By manually changing the display property of the “Create Guide” link from none (hidden) to block (visible), a logged-out user might make the link reappear in their browser. However, even if they can access the form and attempt to create a guide, the Firebase Security Rules will prevent the write operation at the server level because they are not authenticated.

This demonstrates that front-end security measures are for user experience and convenience, not for true security. Server-side security rules are essential for enforcing data access control and preventing unauthorized operations.

Handling Firestore Permission Errors

When a logged-out user attempts to create a guide (even after bypassing front-end UI restrictions), the Firestore operation will fail due to the security rules, resulting in a “missing or insufficient permissions” error. This error is logged to the browser’s console.

Console (Browser DevTools): A tool within web browsers’ developer tools (DevTools) that displays logged messages, errors, and other information related to the execution of JavaScript code and the loading of web page resources.

Uncaught error: An error in JavaScript code that is not handled by a try...catch block or a Promise’s .catch() method, causing the error to propagate up the call stack and potentially halt script execution.

Permissions error (Firebase): An error that occurs when a user or client application attempts to access or modify a Firebase resource (like Firestore data) without having the necessary permissions defined by the Firebase Security Rules.

To provide a better user experience and handle these errors gracefully, we can use a .catch() method on the Promise returned by the add() method. This allows us to intercept any errors that occur during the Firestore operation, including permission errors.

.catch() method (Promise): A method available on Promise objects in JavaScript that allows you to specify a callback function to be executed if the Promise is rejected (fails). This is used for error handling in asynchronous operations.

db.collection('guides').add({
  title: createForm['title'].value,
  content: createForm['content'].value
})
.then(() => {
  // ... (success handling) ...
})
.catch((error) => {
  console.error('Error adding guide:', error.message); // Log the error message
});

Error object (JavaScript): An object in JavaScript that is created to represent an error condition. It typically contains properties like message (a human-readable error description), name (the type of error), and stack (the call stack at the point of error).

.message property (Error object): A property of the JavaScript Error object that contains a human-readable description of the error that occurred.

By adding a .catch() block, we can log a more user-friendly error message to the console (or display it to the user in the UI if desired) instead of the default uncaught error. This enhances the robustness and user experience of our application.

Savvy: Having or showing practical knowledge and experience; shrewd and knowledgeable in the realities of life. In this context, referring to someone with good knowledge of front-end web development techniques.

Conclusion

This chapter demonstrated how to implement a guide creation feature using Firebase Firestore, building upon user authentication and incorporating security best practices. We covered:

  • Setting up the form submission event listener.
  • Interacting with Firestore to add new documents to a collection.
  • Handling asynchronous operations with Promises.
  • Clearing the form and closing the modal after successful submission.
  • Understanding and implementing Firebase Security Rules to protect data.
  • Handling Firestore permission errors gracefully.

This functionality empowers logged-in users to contribute content to the application while ensuring data security through server-side rule enforcement. The next step would be to implement real-time listeners to automatically update the guide list in the UI whenever new guides are added to Firestore, creating a truly dynamic and interactive user experience.

Lock down (Security context): To secure something by restricting access or making it more difficult to breach or compromise. In this context, referring to securing the Firestore database using security rules.

Data (Database context): In the context of a database, ‘data’ refers to the information that is stored and managed within the database. In this chapter, it refers to the guide titles and content stored in Firestore.

Generate ID (Database): The process of automatically creating a unique identifier for a new record or document in a database. Firestore automatically generates unique IDs for documents added using the add() method.

This concludes the educational text derived from the provided transcript.


Implementing Real-time Updates with Firestore Listeners

Enhancing User Experience with Real-time Data

In web applications that display dynamic data, providing users with an immediate and responsive experience is crucial. Previously, in our application, creating a new guide required a manual page refresh to be visible in the list. This is because the application was only fetching data upon changes in user authentication status, such as login or logout. Adding a new guide, an action independent of authentication, did not trigger a data refresh, leading to a less than ideal user experience.

To illustrate, consider the scenario:

  • A user logs in to the application.
  • The application fetches the existing guides from the database and displays them.
  • The user creates a new guide.
  • Without a page refresh, this newly created guide is not immediately visible in the list of guides.
  • The user must manually refresh the page to see the updated list, including the new guide.

This manual refresh requirement disrupts the user workflow and detracts from a seamless and interactive application. Ideally, any changes to the data, such as creating a new guide, should be reflected in the user interface in real-time, without the need for manual intervention.

Leveraging Firestore Real-time Listeners

Fortunately, Firestore, a flexible and scalable database from Firebase, offers a powerful solution to this problem: real-time listeners.

Firestore: A NoSQL document database built for automatic scaling, high performance, and ease of application development. It is part of the Firebase platform and is designed for mobile, web, and server development.

Real-time listener: A mechanism that allows an application to subscribe to changes in a database and receive automatic updates whenever data is modified, added, or deleted.

Firestore’s real-time capabilities enable applications to subscribe to specific parts of the database and receive instant updates whenever changes occur. This eliminates the need for manual data fetching or page refreshes to reflect the latest information. By implementing a real-time listener, we can ensure that our guide list automatically updates whenever a new guide is created, providing a significantly improved user experience.

Implementing onSnapshot() for Real-time Updates

To establish a real-time listener in our application, we can utilize the onSnapshot() method provided by the Firestore API. This method replaces the previous approach of using doc.get() and doc.then(), which only fetched data once.

Here’s how onSnapshot() works to enable real-time updates:

  • Initial Data Retrieval: When onSnapshot() is initially invoked, it immediately fetches the current data from the specified database location (in our case, the collection of guides). This ensures that the application displays the existing data upon loading, just like the previous method.
  • Setting up a Listener: Crucially, onSnapshot() goes beyond initial data retrieval. It establishes a persistent connection to the database and actively listens for any changes within the specified location. This is the core of the real-time functionality.
  • Reacting to Database Changes: Whenever a modification occurs in the database (e.g., a new document is added, an existing document is updated, or a document is deleted), Firestore automatically detects this change and triggers the function provided to onSnapshot().
  • Receiving Updated Snapshots: The function triggered by a database change receives a snapshot.

Snapshot: In the context of Firestore, a snapshot represents a point-in-time view of the data at a specific location in the database. It contains the data and metadata of the documents at the time the snapshot was taken.

This snapshot is essentially an updated “picture” of the data in the collection, reflecting the latest changes. If a new guide has been added, the snapshot will include this new guide.

  • Updating the Application State and UI: With each updated snapshot received, we can then process the new data. In our application, this involves calling the setupGuides() function again, but this time with the updated data from the snapshot. The setupGuides() function is responsible for updating the application’s data model and rendering the guides in the DOM (Document Object Model), which is the structure of the web page.

DOM (Document Object Model): A programming interface for web documents. It represents the page so that programs can change the document structure, style, and content. The DOM represents the document as nodes and objects.

Therefore, whenever a change occurs in the database, onSnapshot() ensures that our application receives the latest data and automatically updates the user interface, reflecting those changes in real-time.

Demonstration: Real-time Guide Creation

To demonstrate the effectiveness of onSnapshot(), consider the following example:

  1. The application is running with the onSnapshot() listener implemented.
  2. A user, logged into the application, navigates to the guide creation interface.
  3. The user fills out the details for a new guide, for instance, “How to Defeat Ganon in Ocarina of Time,” along with a guide description.
  4. Upon clicking the “Create” button, the new guide is saved to the Firestore database.
  5. Because of the real-time listener set up with onSnapshot(), the application instantly detects this database change.
  6. The onSnapshot() function is triggered, receiving an updated snapshot that includes the newly created guide.
  7. The setupGuides() function is called with this updated snapshot.
  8. The new guide automatically appears in the list of guides displayed in the user interface, without requiring a page refresh.

This example showcases the seamless real-time update functionality provided by Firestore listeners. The user experience is significantly enhanced as new data is instantly reflected in the application, creating a more dynamic and responsive interaction.

Conclusion

Implementing real-time listeners with Firestore’s onSnapshot() method offers a straightforward yet powerful way to enhance the user experience in web applications. By subscribing to database changes, applications can provide instant updates, eliminating the need for manual refreshes and creating a more interactive and engaging user interface. This approach is particularly beneficial for applications that display dynamic data, ensuring that users always see the most up-to-date information.


Displaying User Information in the Account Pop-up

This chapter will guide you through the process of displaying user information, specifically the user’s email address, within an account pop-up after a successful login. We will explore how to access user data provided by a user authentication system and dynamically update the user interface (UI) to reflect login status.

Accessing User Data After Login

When a user successfully logs in, an authentication system typically returns a user token.

User Token: A security credential that is granted to a user upon successful authentication. It serves as proof of the user’s identity and allows them to access protected resources.

This token often contains user-specific information. In our scenario, we are interested in retrieving and displaying the user’s email address.

Monitoring Authentication State Changes

To detect when a user logs in or out, we can monitor state changes within our application.

State Change: A modification in the internal data or condition of an application. In the context of authentication, a state change occurs when a user’s login status transitions from logged out to logged in or vice versa.

By observing these state changes, we can execute specific actions, such as updating the UI to reflect the current user’s login status.

Logging User Data to the Console

For debugging and understanding the structure of the user data, we can use console.log() to output the user object to the browser’s developer console.

Console.log(): A function in JavaScript used to print information or messages to the browser’s developer console. It is commonly used for debugging and inspecting variables or program execution flow.

console.log(user);

This allows us to inspect the properties and values contained within the user object. Upon successful login, the console output will reveal the user object, which may contain various user attributes, including the email address.

Displaying the User’s Email Address

Once we have access to the user object, we can extract the email address using the email property.

Email Property: An attribute within the user object that specifically holds the user’s email address as a string value.

The structure of the user object might vary depending on the authentication provider, but commonly, the email address is stored within an email property. For example, to access the email, we can use user.email.

Implementing the setupUI Function

To manage the UI updates based on login status, we can create a function called setupUI. This function will take the user object as an argument and update the UI accordingly.

function setupUI(user) {
  // UI update logic based on user login status
}

This function will be responsible for checking if a user is logged in and, if so, displaying the user’s email in the account pop-up.

Accessing the Account Pop-up Element

To dynamically insert the user’s email into the account pop-up, we first need to obtain a reference to the HTML element that represents the account details section within the pop-up. We can achieve this using document.querySelector() and targeting the account details class.

Account Details Class: A CSS class name assigned to the HTML element within the account pop-up that is designated to display user account information.

Assuming the HTML structure includes an element with the class account-details, we can select it using:

const accountDetails = document.querySelector('.account-details');

This line of code stores a reference to the HTML element in the accountDetails variable.

Constructing the HTML to Display User Information

To display the user’s email, we can construct an HTML string using a template string.

Template String: A feature in JavaScript that allows embedding expressions inside string literals. They are enclosed in backticks () and expressions are placed within ${expression}`.

Template strings provide a cleaner and more readable way to create HTML dynamically. We can create a string that includes a div element to display the “Logged in as” message along with the user’s email:

const html = `
  <div>
    Logged in as: ${user.email}
  </div>
`;

This template string dynamically inserts the user’s email address from the user object into the HTML structure.

Updating the DOM with User Information

To display the constructed HTML within the account pop-up, we need to update the DOM (Document Object Model).

DOM (Document Object Model): A programming interface for HTML and XML documents. It represents the page structure as a tree of objects. JavaScript can use the DOM to dynamically update the content, structure, and style of a web page.

Specifically, we can use the innerHTML property of the accountDetails element to set its content to the generated HTML string.

accountDetails.innerHTML = html;

This line of code inserts the HTML string into the accountDetails element, making the user’s email visible in the account pop-up.

Clearing User Information on Logout

When a user logs out, it’s essential to clear the displayed user information from the account pop-up. We can achieve this by setting the innerHTML of the accountDetails element to an empty string:

accountDetails.innerHTML = '';

This removes any existing content within the account details section, ensuring that no user information is displayed when the user is logged out.

Handling Potential Errors

In some scenarios, like after logout, attempting to access user data might result in errors, especially when dealing with features like database snapshots.

Snapshot: In the context of databases, a snapshot is an immutable copy of data at a specific point in time. Attempting to access data through a snapshot after logout might lead to permission issues if the user session or associated permissions are no longer valid.

If we encounter errors due to insufficient permissions after logout, we can implement error handling using try...catch blocks.

Permissions: Authorizations granted to a user or process to access specific resources or perform certain actions within a system. Insufficient permissions indicate that the user or process does not have the necessary authorization for the requested operation.

try {
  // Code that might cause a permission error (e.g., accessing snapshot data after logout)
} catch (error) {
  console.log(error.message); // Log the error message to the console
}

This error handling mechanism helps prevent unhandled exceptions and provides a way to gracefully manage potential errors, such as those related to data access after logout.

Conclusion

By following these steps, we can effectively display user information, specifically the email address, within an account pop-up upon successful login. This involves accessing user data, dynamically updating the DOM, and handling potential errors. This process enhances the user experience by providing clear visual feedback about their logged-in status and account details.


User Data Management with Firebase: Storing Custom User Information in Firestore

This chapter explores how to effectively manage user data within a Firebase project, focusing on strategies for storing custom user information beyond the basic authentication details. We will delve into utilizing Firebase Authentication and Firestore Database in conjunction to create a robust and efficient user data management system.

Introduction to User Data in Firebase

Firebase offers powerful services for user authentication and data storage. Firebase Authentication excels at managing user identities, including sign-up, sign-in, and basic user profile information. This information typically includes:

  • User ID (UID): A unique identifier automatically assigned to each user upon registration.
  • Email Address: The user’s registered email.
  • Profile Information: Optional fields such as:
    • Photo URL
    • Display Name

However, Firebase Authentication is not designed for storing extensive custom user data. For richer user profiles and application-specific data, we need to leverage Firebase’s database solutions, specifically Firestore.

Firebase Authentication: A service provided by Firebase that handles user identity management, including user sign-up, sign-in, and account security. It stores basic user information like UID, email, and profile details.

Storing Custom User Data in Firestore

When your application requires storing additional user information beyond the basics offered by Firebase Authentication, such as user biographies, preferences, or settings, Firestore becomes the ideal solution. Firestore is a NoSQL document database that provides a flexible and scalable way to store and retrieve data.

Firestore: A flexible, scalable NoSQL cloud database to store and sync data for client- and server-side development. It is part of the Firebase suite of services and is designed for mobile and web applications.

The recommended approach is to create a dedicated collection in Firestore to store user-specific custom data. A common practice is to name this collection “users”. Within this collection, each user’s data is stored as a document.

Collection: In Firestore, a collection is a group of documents. It’s used to organize and structure your data. Collections contain documents, and can also contain subcollections.

Document: In Firestore, a document is a lightweight record that contains fields which map to values. It’s the unit of storage in Firestore. Documents are stored in collections.

Linking Authentication Data with Firestore Data

To effectively link user authentication information with their custom data in Firestore, we can utilize the User ID (UID) generated by Firebase Authentication. The UID serves as a common identifier across both services.

Instead of relying on Firestore’s automatic document ID generation, we can explicitly set the document ID in the “users” collection to be the same as the user’s UID from Firebase Authentication. This establishes a clear and direct relationship between the authentication record and the custom data document.

Benefits of this approach:

  • Synchronization: The UID acts as a bridge, connecting the authentication information and the custom data.
  • Efficient Data Retrieval: When a user is logged in and their UID is known, we can easily query Firestore to retrieve their specific custom data document using the UID as the document ID.
  • Logical Data Separation: Keeps basic authentication data separate from more extensive, application-specific user information.

Why Separate Authentication and Custom Data?

You might initially consider storing all user information within Firebase Authentication itself. However, there are significant reasons to separate custom data into Firestore:

  • Data Payload Efficiency: Every request made to Firebase services involves data transfer. Storing large amounts of custom data (like biographies, preferences, etc.) directly within the authentication record would mean unnecessarily transmitting this data with every authentication request, even when it’s not needed.
  • Performance Optimization: Retrieving large authentication records repeatedly can impact application performance. Separating data allows you to fetch only the necessary custom data when it’s specifically required, for example, when displaying a user profile page.
  • Scalability and Flexibility: Firestore is designed for handling large volumes of data and complex queries, making it more suitable for managing evolving and expanding user profiles compared to the more limited scope of Firebase Authentication.

Implementing Custom User Data Storage: A Step-by-Step Guide

Let’s walk through the process of adding a “biography” field to user profiles and storing it in Firestore. We’ll assume a signup form that currently collects email and password, and we’ll add a new field for a one-line biography.

1. Adding the Biography Field to the Signup Form (HTML)

First, modify your HTML signup form to include an input field for the user’s biography. This involves adding a new input element with appropriate attributes:

<div class="input-field">
    <input type="text" id="signup-bio">
    <label for="signup-bio">One-Line Bio</label>
</div>

This HTML snippet adds a text input field with the ID signup-bio and a corresponding label, allowing users to enter their biography during signup.

2. Updating the Signup Function (JavaScript)

Next, update your JavaScript code that handles user signup to include the logic for storing the biography in Firestore. This involves modifying the existing signup function to perform the following steps after successful user creation via Firebase Authentication:

  1. Retrieve the User ID: After successfully creating a user with createUserWithEmailAndPassword, access the newly generated User ID (UID) from the authentication response.

  2. Access Firestore: Obtain a reference to your Firestore database instance.

  3. Create a Document Reference: Use the collection() and doc() methods to create a reference to a specific document within the “users” collection. Crucially, use the UID obtained in step 1 as the document ID.

    Method: In programming, a method is a function that is associated with an object. It defines the actions that can be performed on or by that object. In this context, collection() and doc() are methods of a Firestore database object.

  4. Set the Biography Data: Use the set() method on the document reference to add the biography data. This method takes an object as an argument, where the keys represent field names in the document, and the values represent the data to be stored. In our case, we’ll set a “bio” field with the value from the signup-bio input field.

    Object: In programming, an object is a collection of key-value pairs. In JavaScript, objects are used to represent data structures and can contain properties (keys) that hold values of various data types.

    Property: In programming, a property is a characteristic or attribute of an object. In JavaScript objects, properties are key-value pairs. In this context, “bio” is a property of the document object we are creating in Firestore.

  5. Handle Success: Implement a .then() promise to execute code after the Firestore write operation is successful. This could include actions like closing the signup modal and resetting the form.

    Promise: In JavaScript, a promise is an object that represents the eventual result of an asynchronous operation. It can be in one of three states: pending, fulfilled, or rejected. Promises are used to handle asynchronous operations in a more structured and readable way.

Here’s an example of the JavaScript code snippet illustrating these steps:

// ... (Existing signup code to get email, password, and create user with Firebase Authentication) ...

const signupBio = document.getElementById('signup-bio').value;

createUserWithEmailAndPassword(auth, email, password)
    .then((userCredential) => {
        const user = userCredential.user;
        const uid = user.uid;

        // Access Firestore database (assuming 'db' is your Firestore instance)
        const db = getFirestore();

        // Create a document reference in the 'users' collection with UID as document ID
        const userDocRef = doc(collection(db, 'users'), uid);

        // Set the 'bio' field in the document
        return setDoc(userDocRef, {
            bio: signupBio
        });
    })
    .then(() => {
        // Actions to perform after successful Firestore write (e.g., close modal, reset form)
        console.log("User data successfully written to Firestore");
        // ... (Code to close modal and reset form) ...
    })
    .catch((error) => {
        console.error("Error signing up or writing to Firestore: ", error);
        // ... (Error handling) ...
    });

Explanation of Key Code Snippets:

  • doc(collection(db, 'users'), uid): This line constructs a reference to a specific document. collection(db, 'users') gets a reference to the “users” collection in Firestore, and doc(..., uid) specifies that we want a document within that collection with the ID equal to the uid.
  • setDoc(userDocRef, { bio: signupBio }): This line uses the setDoc function (or set method in older Firebase versions) to write data to the document referenced by userDocRef. It sets a field named “bio” with the value of signupBio.
  • .then(() => { ... }): This is a promise’s then handler. It executes the code inside the function only after the preceding asynchronous operation (in this case, setDoc) has successfully completed.

3. Firestore Security Rules (Considerations)

Initially, for testing and development, you might configure your Firestore Security Rules to allow read and write access without authentication. However, for production applications, it is crucial to implement proper security rules to protect user data. You should configure rules that restrict access to the “users” collection based on user authentication and authorization.

Displaying Custom User Data

Once the custom user data is stored in Firestore, you can retrieve and display it in your application. For instance, to display the biography on a user’s profile page:

  1. Get User ID: Obtain the currently logged-in user’s UID from Firebase Authentication.

  2. Query Firestore: Use the UID to construct a document reference to the user’s document in the “users” collection in Firestore.

  3. Retrieve Document Data: Use the get() method on the document reference to fetch the document data. This is an asynchronous operation, so use .then() to handle the response.

  4. Display Data: Extract the “bio” field (or any other custom data field) from the retrieved document data and display it in your application’s UI.

Here’s an example of fetching and displaying the biography in JavaScript:

// ... (Assume you have access to the currently logged-in user object 'user') ...

const uid = user.uid;
const db = getFirestore();
const userDocRef = doc(collection(db, 'users'), uid);

getDoc(userDocRef)
    .then((docSnapshot) => {
        if (docSnapshot.exists()) {
            const userData = docSnapshot.data();
            const bio = userData.bio; // Access the 'bio' field
            // Display the biography in your UI (e.g., using innerHTML)
            document.getElementById('bio-display').innerHTML = bio;
        } else {
            console.log("No such document!");
        }
    })
    .catch((error) => {
        console.error("Error getting document:", error);
    });

Explanation of Key Code Snippets:

  • getDoc(userDocRef): This line fetches the document data from Firestore. getDoc is a function that takes a document reference and returns a promise that resolves with a DocumentSnapshot.
  • .then((docSnapshot) => { ... }): This is the promise’s then handler. It executes when the getDoc operation is successful, providing a DocumentSnapshot object as the argument.
  • docSnapshot.exists(): Checks if a document exists at the referenced location.
  • docSnapshot.data(): If the document exists, docSnapshot.data() returns an object containing the data in the document.
  • userData.bio: Accesses the “bio” field from the retrieved document data.

Event Listener: In programming, particularly in web development with JavaScript, an event listener is a procedure or function that waits for an event to occur. When the event happens, the listener executes a predefined response, often a callback function. In the transcript, an event listener is attached to the signup form to handle the form submission event.

Callback Function: In programming, a callback function is a function passed as an argument to another function. It is designed to be executed at a later point in time, often after an asynchronous operation completes or in response to an event. In the transcript, callback functions are used within promise .then() and .catch() methods to handle the results of asynchronous Firebase operations.

Snapshot: In Firebase, a snapshot is a point-in-time view of data. In the context of Firestore, DocumentSnapshot represents a snapshot of a document at a specific time. It contains the document data, metadata, and methods to check for document existence and changes.

Real-time database: While not explicitly used in this context (Firestore is used), the transcript mentions setting up a real-time database listener previously. A real-time database, in general, is a database that pushes data updates to connected clients in real-time, allowing for live synchronization and immediate data reflection across applications. Firebase Realtime Database is another Firebase service that provides this functionality, distinct from Firestore.

HTML (HyperText Markup Language): The standard markup language for creating web pages and web applications. HTML uses tags to structure content, define elements, and embed multimedia. In the transcript, HTML is used to create the signup form and the input field for the biography.

UID (User ID): A unique identifier assigned by Firebase Authentication to each user who registers or signs in. This ID is used to uniquely identify users within the Firebase project and is crucial for linking user data across different Firebase services like Authentication and Firestore.

Asynchronous: In programming, asynchronous operations are non-blocking operations that allow the program to continue executing other tasks while waiting for the operation to complete. Promises, as used in the transcript with Firebase operations like createUserWithEmailAndPassword and getDoc, are a common way to manage asynchronous operations in JavaScript.

Conclusion

By strategically combining Firebase Authentication and Firestore, you can create a comprehensive and efficient user data management system. Storing custom user data in Firestore, linked to Firebase Authentication via the User ID, offers benefits in terms of data organization, performance, scalability, and data payload efficiency. This approach ensures that your application can handle both basic authentication and rich user profile data effectively.


Securing User Data in Firebase Firestore: Implementing Granular Access Control

This chapter explores the crucial aspect of securing user data within Firebase Firestore, a NoSQL document database. We will delve into how to implement specific security rules to control access to user information, ensuring data privacy and integrity. This is essential for any application that manages user profiles and sensitive data.

Understanding the Initial Security Configuration

Initially, Firebase projects are often configured with open security rules to facilitate development and testing. However, these permissive rules are unsuitable for production environments as they can expose sensitive data to unauthorized access.

In our starting scenario, the Firebase security rules for the ‘users’ collection are broadly defined, allowing unrestricted read and write access. Let’s examine the implications of these initial rules based on the transcript:

Initially, the security rules are set to allow read and write operations on the ‘users’ collection without any conditions.

This means that, by default, anyone with access to the Firebase project, whether authenticated or not, could potentially:

  • Read any user’s data: Access personal information, profiles, or any data stored within user documents in the ‘users’ collection.
  • Modify any user’s data: Alter user profiles, bios, or any data fields, potentially leading to data corruption or unauthorized changes.

This level of access is clearly undesirable for a secure application. Therefore, we need to implement more restrictive rules to protect user data.

Defining Granular Security Objectives

Our goal is to refine the security rules for the ‘users’ collection to achieve the following objectives:

  • Authenticated User Document Creation: Allow only authenticated users to create new documents within the ‘users’ collection. This is necessary because during user signup, after Firebase Authentication is successful, a corresponding user document needs to be created in Firestore to store user-specific data, such as a bio.

Firebase Authentication (Firebase Auth) Firebase Authentication provides backend services, SDKs, and UI libraries to authenticate users to your app. It supports authentication using passwords, phone numbers, popular federated identity providers like Google, Facebook and Twitter, and more.

  • Self-Document Read Access: Restrict read access to user documents so that authenticated users can only read their own documents and not those of other users. This ensures that users can access their profiles and information but cannot view the data of others, maintaining user privacy.

These objectives establish a balance between functionality and security. New users can be created and store their information, while unauthorized access to user data is prevented.

Implementing Security Rules for Granular Access Control

To achieve the defined objectives, we need to modify the Firebase security rules. Firebase security rules are written in a declarative language that dictates how data in your Firestore database can be accessed. Let’s break down the rule implementation step-by-step, referencing the transcript’s example:

  1. Targeting the ‘users’ Collection and Individual User Documents:

    We begin by specifying the path within Firestore that our rules will apply to. We use the match keyword to target the ‘users’ collection and then further refine it to target individual documents within this collection.

    match /users/{user ID} {
        // Rules will be defined here
    }

    match (in Firebase Security Rules) The match statement in Firebase Security Rules is used to specify paths in your Firestore database to which the rules defined within the block will apply. It allows you to target collections and documents for specific access control.

    In this rule, /users/{user ID} targets any document within the ‘users’ collection. The {user ID} is a wildcard that acts as a variable, representing the dynamic document ID being requested. This is crucial because we want to apply rules based on the specific document being accessed.

    Document ID In Firestore, each document within a collection has a unique identifier called the Document ID. This ID can be automatically generated by Firestore or specified by the user when creating the document.

  2. Allowing Document Creation for Authenticated Users:

    To permit authenticated users to create new user documents, we use the allow create rule within the match block.

    match /users/{user ID} {
        allow create: if request.auth.uid != null;
        // ... other rules
    }

    allow create (in Firebase Security Rules) The allow create rule grants permission to create new documents at the specified path in Firestore. It is typically used in conjunction with conditions to control who can create documents.

    request.auth.uid (in Firebase Security Rules) request.auth.uid is a variable available within Firebase Security Rules that represents the unique user ID (UID) of the currently authenticated user making the request. If no user is authenticated, this value is null.

    null (in programming) In programming, null represents the intentional absence of a value or object reference. It indicates that a variable or property has no value assigned to it.

    The condition request.auth.uid != null checks if a user is authenticated. If request.auth.uid is not equal to null, it means a user is logged in, and therefore, the create operation is allowed.

  3. Restricting Read Access to Own Documents:

    To ensure users can only read their own documents, we implement the allow read rule with a specific condition.

    match /users/{user ID} {
        allow create: if request.auth.uid != null;
        allow read: if request.auth.uid == user ID;
    }

    allow read (in Firebase Security Rules) The allow read rule grants permission to read documents at the specified path in Firestore. Conditions are often used to restrict read access based on user authentication or document content.

    The condition request.auth.uid == user ID is the core of this security measure. request.auth.uid represents the authenticated user’s ID, and user ID refers to the document ID being requested (due to the {user ID} wildcard in the match statement).

    This rule effectively checks if the authenticated user’s ID is the same as the ID of the document they are trying to read. If they match, meaning the user is trying to access their own document, read access is granted. Otherwise, if the IDs don’t match, read access is denied.

  4. Understanding write vs. create:

    It’s important to note the distinction between write and create rules. While create specifically controls the creation of new documents, write encompasses both creation and modification (updating existing documents). In our scenario, we only needed to allow document creation initially, and further rules could be added to control updates later if required.

    write (in Firebase Security Rules) The allow write rule grants permission for both creating and modifying documents at the specified path in Firestore. It is a broader permission than allow create or allow update individually.

Deploying and Testing the Security Rules

After defining these rules in the Firebase console, they need to be deployed, or “published,” for them to take effect in the live application.

publish (in Firebase context) In the context of Firebase Security Rules, “publishing” refers to the process of deploying the rules you have written to your Firebase project. Once published, these rules are actively enforced for all data access requests to your Firestore database.

Once published, it is crucial to test the rules thoroughly to ensure they function as expected. This involves simulating different scenarios, such as:

  • User signup and document creation: Verify that a new user signup still successfully creates a corresponding document in the ‘users’ collection with the correct data.
  • Reading own document: Confirm that an authenticated user can successfully read their own user document.
  • Attempting to read another user’s document: Ensure that an authenticated user is prevented from reading another user’s document, thereby validating the read access restriction.

By performing these tests, we can verify that the implemented security rules effectively protect user data while maintaining the necessary application functionality.

Conclusion

Implementing granular security rules in Firebase Firestore is paramount for protecting sensitive user data. By carefully defining match statements and using conditions based on authentication status (request.auth.uid) and document IDs (user ID), we can establish precise control over data access. The rules outlined in this chapter restrict document creation to authenticated users and limit read access to only the document owner, significantly enhancing the security and privacy of user data within the application. This approach demonstrates a fundamental step towards building secure and trustworthy applications with Firebase.


User Roles and Custom Claims in Firebase Authentication

This chapter explores the concept of user roles and how to implement them using custom claims within Firebase Authentication. We will delve into the need for differentiating user privileges and how Firebase’s custom claims offer a robust solution for managing user permissions. Furthermore, we will introduce Firebase Cloud Functions as a secure method for setting these claims and preparing for their implementation in subsequent sections.

Introduction to User Roles and Privileges

In many applications, it’s essential to distinguish between different types of users based on their roles and the privileges they possess. This allows for controlled access to application features and data. In the context of our guide application, we aim to differentiate between standard users and administrators.

  • Standard Users: Upon signing up, standard users should be able to access and view existing guides.
  • Administrators: Administrators, a special type of user, should have elevated privileges, including the ability to create new guides in addition to viewing them.

This distinction requires a mechanism to identify user types and enforce role-based access control.

Leveraging Custom Claims for User Management

To differentiate between user roles and grant special privileges, Firebase Authentication offers a feature called custom claims.

Custom Claims: These are additional pieces of information associated with a user’s Firebase authentication token, beyond the standard user properties like user ID, email, and display name. Custom claims are used to store user roles, permissions, or any other user-specific attributes that are relevant for authorization and access control.

Understanding Claims

Claims are essentially extra data points that can be attached to a user’s profile within Firebase Authentication. While Firebase provides default user properties like uid (user ID), email, and display name, custom claims extend this by allowing developers to add specific, application-defined attributes.

  • Examples of Custom Claims:
    • An admin claim (boolean value: true or false) to denote administrator status.
    • A premium claim (boolean value: true or false) to indicate a user’s premium subscription.

Best Practices for Custom Claims

It’s crucial to understand the intended use of custom claims and avoid misuse.

  • Appropriate Use: Custom claims are designed for storing user roles, permissions, and similar authorization-related data.
  • Inappropriate Use: Custom claims are not intended for storing general user data like biographies or blog post history. Storing excessive data in custom claims is inefficient because the user token, which includes these claims, is transmitted with every request to Firebase. This can increase token size and impact performance.

Firebase Authentication Token: A security token issued by Firebase Authentication after a user successfully signs in. This token is used to verify the user’s identity in subsequent requests to Firebase services and contains information about the user, including any custom claims.

For general user data that is not directly related to roles or permissions, it is recommended to utilize Firebase Firestore, as discussed in previous materials.

Benefits of Custom Claims

Using custom claims offers several advantages in managing user roles and permissions:

  • Access Control: Custom claims enable the implementation of role-based access control. You can define rules that grant or deny access to specific resources or functionalities based on a user’s claims.
  • Dynamic UI Updates: Custom claims can be used to dynamically adjust the user interface (UI) based on a user’s role. For example, administrators might see additional UI elements or options compared to standard users.
  • Database Security Rules: Firebase Security Rules can be configured to enforce access restrictions based on custom claims. This allows for server-side security that ensures only authorized users can perform certain operations on the database.

Securely Setting Custom Claims with Cloud Functions

Setting custom claims is a sensitive operation that should be performed securely to prevent unauthorized privilege escalation.

The Importance of Server-Side Claim Setting

It is critical to set custom claims on the server-side, rather than directly from the client-side (e.g., within a web browser or mobile app). Client-side claim setting would be inherently insecure as it could be manipulated by malicious users to grant themselves elevated privileges.

Introduction to Firebase Cloud Functions

To securely set custom claims, we will utilize Firebase Cloud Functions.

Firebase Cloud Functions: A serverless compute platform provided by Firebase that allows you to run backend code in response to events triggered by Firebase and Google Cloud services. Cloud Functions are useful for executing server-side logic without managing your own servers.

Cloud Functions provide a secure environment to execute backend code that should not be exposed to the client. They are ideal for tasks such as:

  • Secure Operations: Performing operations that require elevated privileges or sensitive data handling, like setting custom claims.
  • Backend Logic: Implementing complex business logic or integrations with third-party services.
  • Event-Driven Tasks: Responding to events within Firebase, such as user creation or database changes.

While Cloud Functions run on the server, they can be invoked from the client-side under controlled conditions. For instance, to make a user an administrator, a client application (with appropriate authorization) can call a Cloud Function that then securely sets the admin custom claim for that user.

Setting Up Cloud Functions Locally

Before deploying Cloud Functions to Firebase, it’s essential to set up a local development environment. This allows for testing and development before deploying to production.

Installing Firebase CLI Tools

To work with Firebase Cloud Functions locally, you need the Firebase Command Line Interface (CLI) tools.

Firebase CLI Tools: A set of command-line tools provided by Firebase that enables developers to interact with Firebase projects from their terminal. This includes functionalities like deploying applications, managing databases, and working with Cloud Functions.

  • Installation Command: Open your terminal and execute the following command to install the Firebase CLI tools globally using Node Package Manager (npm):

    npm install -g firebase-tools

    The -g flag ensures that the tools are installed globally, making them accessible from any directory in your terminal.

Initializing Cloud Functions in Your Project

Once the Firebase CLI tools are installed, navigate to your project directory in the terminal and initialize Cloud Functions.

  • Initialization Command: Execute the following command:

    firebase init functions
  • Project Selection: The CLI will prompt you to select a Firebase project. Choose the project you created for your application (e.g., “nm game guides”). This links your local functions environment to your Firebase project.

  • Language Selection: You will be asked to choose a language for writing your Cloud Functions. Select “JavaScript” (or “TypeScript” if preferred).

  • ESLint Setup: You will be prompted about ESLint. Choose “no” for now.

  • Dependency Installation: You will be asked to install dependencies with npm. Choose “yes” and press Enter. This will install necessary packages for Cloud Functions within a functions folder in your project directory.

Examining the Project Structure

After successful initialization, you will observe the following changes in your project structure:

  • firebase.json: This file contains project-level configuration settings for Firebase, including settings related to Cloud Functions.
  • functions folder: This newly created folder houses all the files related to your Cloud Functions.
    • node_modules: Contains the installed dependencies for your Cloud Functions.
    • package.json: Defines the dependencies and scripts for your Cloud Functions.
    • index.js: This is the main file where you will write your Cloud Functions code. It typically includes a pre-configured example function as a starting point.

Next Steps

In the subsequent sections, we will focus on creating a callable Cloud Function within index.js. This function will be designed to securely set a custom admin claim for a user, enabling the implementation of administrator privileges in our application. We will explore how to deploy this function to Firebase and integrate it into our application’s authentication and authorization flow.


Creating a Cloud Function to Set Admin Roles in Firebase

This chapter will guide you through the process of creating a Firebase Cloud Function to assign admin roles to users. We will cover the necessary setup, code implementation, deployment, and basic testing of this function. This is a crucial aspect of backend development for applications requiring role-based access control.

1. Introduction to Cloud Functions and Firebase Admin SDK

In the previous discussion, we initialized functions on the front end of our application. Now, we will focus on building a cloud function to handle administrative tasks, specifically setting a user as an admin.

Cloud Function: Cloud Functions are serverless functions that execute in response to events such as changes to Firebase data or HTTP requests. They allow you to run backend code without managing servers.

Cloud Functions operate within a Node environment when deployed to Firebase.

Node Environment: A runtime environment that allows JavaScript code to be executed server-side. It’s commonly used for building scalable and efficient backend applications.

This environment necessitates the use of require to import necessary modules. We begin by importing the firebase-functions module, which is essential for creating and deploying our cloud function.

const functions = require('firebase-functions');

Next, we need to incorporate the Firebase Admin SDK to interact with Firebase services with administrative privileges.

Firebase Admin SDK: A set of libraries that allows you to interact with Firebase services from privileged environments like Cloud Functions or trusted servers. It bypasses security rules and grants full access to your Firebase project’s data and services.

The Admin SDK allows us to initialize the app server-side and utilize services like the authentication service.

Authentication Service: A service that manages user identity and authentication within Firebase. It allows you to create, manage, and authenticate users in your application.

The authentication service, through the Admin SDK, enables us to retrieve user information and apply custom claims.

Custom Claims: A mechanism to add custom attributes or roles to a user’s Firebase authentication token. These claims can be used to implement role-based access control and other authorization logic in your application.

To utilize these capabilities, we import and initialize the Firebase Admin SDK:

const admin = require('firebase-admin');
admin.initializeApp();

With the Admin SDK initialized, we are now equipped to access services like authentication and perform administrative actions such as setting custom claims.

2. Creating the addAdminRole Cloud Function

We will now create a cloud function named addAdminRole that will add an admin role to a specified user. Cloud Functions are structured using the exports object in Node.js.

2.1 Function Structure and Type

To create our function, we add it to the exports object. The function will be named addAdminRole. We will use the functions object (imported from firebase-functions) to define the type of function. In this case, we are creating an HTTPS function that is also an on-call function.

HTTPS Function: A type of Cloud Function that is triggered by HTTPS requests. It can be invoked by sending HTTP requests to a specific endpoint provided by Firebase.

On-call Function: A specific type of HTTPS function designed to be called directly from client-side applications (like web or mobile apps) using Firebase SDKs. It simplifies calling backend logic from the frontend.

exports.addAdminRole = functions.https.onCall((data, context) => {
  // Function logic will be placed here
});

2.2 Function Parameters: data and context

The onCall function accepts a callback function as a parameter.

Callback Function: A function passed as an argument to another function, to be executed at a later point, often after an asynchronous operation completes.

This callback function receives two parameters: data and context.

  • data: This object contains any custom data sent from the client-side when calling the function. In our case, this will include the email address of the user we want to make an admin.

  • context: This object provides information about the context of the function call, including authentication details of the user who initiated the call. While we won’t be using the context object extensively in this example, it’s important to know it exists for potential future use cases like verifying the caller’s permissions before executing the function.

2.3 Function Logic: Getting User and Setting Custom Claim

Inside the addAdminRole function, our goal is to:

  1. Get the user based on the email address provided in the data object.
  2. Add a custom claim named “admin” to this user.

First, we retrieve the user using the Admin SDK’s authentication service (admin.auth()) and the getUserByEmail method. We pass data.email to this method to specify which user to retrieve.

return admin.auth().getUserByEmail(data.email)

This getUserByEmail method returns a promise.

Promise: An object representing the eventual outcome of an asynchronous operation. It can be in one of three states: pending, fulfilled (successful), or rejected (failed). Promises are used to handle asynchronous operations in JavaScript in a more structured way.

Since it returns a promise, we use .then() to handle the successful retrieval of the user. The .then() method takes a callback function that receives the retrieved user object as a parameter.

.then((user) => {
  // Logic to set custom claim
})

Inside the .then() block, we use admin.auth().setCustomUserClaims() to set the custom claim. This method requires two parameters:

  1. user.uid: The UID (User ID) of the user to whom we want to apply the custom claim.

    UID (User ID): A unique identifier assigned to each user in Firebase Authentication. It’s used to distinguish users within your Firebase project.

  2. Claims Object: An object representing the custom claims to be set. In our case, we want to set the admin claim to true.

return admin.auth().setCustomUserClaims(user.uid, {
  admin: true
})

setCustomUserClaims also returns a promise. We chain another .then() to handle the completion of setting the custom claim and to return a response to the client.

.then(() => {
  return {
    message: `Success! ${data.email} has been made an admin.`
  };
})

Finally, we include a .catch() block to handle any potential errors during the process. This is crucial for robust error handling.

.catch((error) => {
  return error;
});

The complete addAdminRole cloud function code becomes:

exports.addAdminRole = functions.https.onCall((data, context) => {
  return admin.auth().getUserByEmail(data.email)
    .then((user) => {
      return admin.auth().setCustomUserClaims(user.uid, {
        admin: true
      });
    })
    .then(() => {
      return {
        message: `Success! ${data.email} has been made an admin.`
      };
    })
    .catch((error) => {
      return error;
    });
});

3. Deploying the Cloud Function

Once the cloud function is written, we need to deploy it to Firebase. Deployment is typically done using the Firebase CLI (Command Line Interface).

Firebase CLI (Command Line Interface): A tool for managing your Firebase projects from the command line. It allows you to deploy code, manage configurations, and interact with various Firebase services.

To deploy only the functions, we use the following command in the terminal within the functions directory of your Firebase project:

firebase deploy --only functions

This command will package and deploy your addAdminRole function to Firebase.

Deploy: The process of uploading and activating your Cloud Functions code to the Firebase server infrastructure, making them accessible and executable.

After successful deployment, you will see a “Deploy complete!” message in the terminal.

4. Verifying Deployment and Function Logs

To verify that the function has been deployed correctly, navigate to the Firebase console for your project. Go to the “Functions” section. You should see the addAdminRole function listed there.

To monitor the function’s execution and any potential errors, you can check the “Logs” tab within the Functions section in the Firebase console. Every time the function is called, logs will be generated here, providing insights into its execution flow and any issues that might arise.

5. Next Steps: Calling the Function from the Front End

With the addAdminRole cloud function deployed, the next step is to integrate it into your front-end application. This will involve calling this function from your client-side code, passing the user’s email address as data, and handling the response. This process will be covered in detail in the subsequent chapter.


Calling Cloud Functions from the Frontend: A Practical Guide

This chapter will guide you through the process of invoking Google Cloud Functions from your web application’s frontend, using Firebase. We will build upon the concept of a “Make Admin” Cloud Function, allowing you to elevate user privileges directly from your application’s user interface.

Setting Up the Frontend Interface

To initiate the process of making a user an administrator, we first need to create a user interface element in our web application. This will allow users, specifically administrators, to input the email address of the user they wish to promote.

Implementing the HTML Form

We will add a simple HTML form to our index.html file. This form will contain an input field for the user’s email and a button to trigger the admin promotion process.

<div class="admin-actions center-aligned">
    <form id="admin-form">
        <input type="email" id="admin-email" placeholder="User Email" required>
        <button type="submit">Make Admin</button>
    </form>
</div>

This HTML snippet creates a form with the class admin-actions for styling purposes and the ID admin-form for JavaScript interaction. Inside the form:

  • An <input> field of type “email” is included.
    • id="admin-email": This ID will be used to access this input element using JavaScript.
    • placeholder="User Email": Provides a hint to the user about the expected input.
    • required: Ensures that the user must enter an email address before submitting the form.
  • A <button> of type “submit” is included.
    • When clicked, this button will submit the form.

Basic styling is applied to center the form and limit its width for better presentation:

.admin-actions {
    margin: 40px auto;
    max-width: 300px;
    text-align: center; /* To center the button */
}

.center-aligned {
    display: flex;
    justify-content: center;
}

.center-aligned form {
    display: flex;
    flex-direction: column; /* Stack form elements vertically */
    align-items: stretch; /* Stretch form elements to full width */
}

.center-aligned input, .center-aligned button {
    margin-bottom: 10px; /* Add some spacing between elements */
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
}

.center-aligned button {
    background-color: #007bff;
    color: white;
    border: none;
    cursor: pointer;
}

.center-aligned button:hover {
    background-color: #0056b3;
}

HTML (HyperText Markup Language): The standard markup language for documents designed to be displayed in a web browser. It can be assisted by technologies such as Cascading Style Sheets (CSS) and scripting languages such as JavaScript.

CSS (Cascading Style Sheets): A style sheet language used for describing the presentation of a document written in a markup language like HTML. CSS describes how HTML elements should be displayed on screen, paper, or in other media.

Integrating Firebase Functions SDK in the Frontend

To interact with Cloud Functions from our frontend application, we need to initialize the Firebase Functions Software Development Kit (SDK). This SDK provides the necessary tools and libraries to communicate with Firebase services, including Cloud Functions.

Initializing Firebase Functions

In your JavaScript file (e.g., auth.js or app.js), ensure you have already initialized the core Firebase application and other services you are using (like Authentication and Firestore). Then, add the following lines to initialize the Functions SDK:

// Get a reference to Firebase Functions
const functions = firebase.functions();

SDK (Software Development Kit): A collection of software development tools in one installable package. They often include libraries, documentation, code samples, processes, and guides that aid developers in creating software applications for a specific platform or API.

This line of code retrieves a reference to the Firebase Functions service, making it available for use in your frontend code. We store this reference in a constant variable named functions.

Calling the Cloud Function

Now that we have set up the frontend interface and initialized the Functions SDK, we can proceed to write the JavaScript code to call our “Add Admin Role” Cloud Function when the “Make Admin” button is clicked.

Handling Form Submission

We need to attach an event listener to our form to detect when it is submitted. This will allow us to intercept the submission, prevent the default page reload behavior, and execute our custom logic to call the Cloud Function.

// Get a reference to the admin actions form
const adminForm = document.querySelector('#admin-form');

// Add event listener for form submission
adminForm.addEventListener('submit', (e) => {
  e.preventDefault(); // Prevent default form submission
  // ... rest of the code to call cloud function ...
});

Event Listener: A procedure in JavaScript that waits for a specific event to occur and then executes a predefined function in response to that event. Common events include user interactions like clicks, form submissions, and page loads.

Submit Event: An event that is triggered when a form is submitted, typically when a submit button within the form is clicked. By default, submitting a form in a web browser will cause the page to reload or navigate to a new page.

e.preventDefault(): A method called on an event object in JavaScript. It stops the default action of the event from happening. In the context of a form’s submit event, e.preventDefault() prevents the form from submitting in the traditional way, which would cause a page reload.

Here, we first get a reference to the form element using document.querySelector('#admin-form').

document.querySelector(): A JavaScript method that allows you to select the first element that matches a specified CSS-style selector in the document. It is a powerful tool for accessing and manipulating HTML elements using JavaScript.

Then, we use addEventListener('submit', ...) to attach a function that will be executed when the form is submitted. The e parameter in the callback function is the event object, and e.preventDefault() is called to prevent the default form submission behavior, which would cause a page reload.

Retrieving User Email

Inside the submit event listener, we need to retrieve the email address entered by the user in the input field. We can do this using document.querySelector() again, targeting the input field with the ID admin-email.

adminForm.addEventListener('submit', (e) => {
  e.preventDefault();

  // Get the admin email from the input field
  const adminEmail = document.querySelector('#admin-email').value;

  // ... code to call cloud function with adminEmail ...
});

Here, document.querySelector('#admin-email').value retrieves the current value entered in the input field with the ID admin-email. We store this value in the adminEmail constant.

Making the HTTP Callable Function Call

To call our Cloud Function, we utilize the httpsCallable function provided by the Firebase Functions SDK. This function allows us to call callable Cloud Functions, which are specifically designed to be invoked directly from client-side applications via HTTPS.

adminForm.addEventListener('submit', (e) => {
  e.preventDefault();
  const adminEmail = document.querySelector('#admin-email').value;

  // Create a reference to the 'addAdminRole' cloud function
  const addAdminRole = functions.httpsCallable('addAdminRole');

  // Call the 'addAdminRole' cloud function, passing the admin email
  addAdminRole({ email: adminEmail })
    .then((result) => {
      // Handle successful function call
      console.log('Success:', result.data.message); // Access the message returned from the function
    })
    .catch((error) => {
      // Handle errors
      console.error('Error calling cloud function:', error);
    });
});

firebase.functions(): This is the entry point to the Firebase Functions service from the Firebase SDK. It allows you to access and interact with your deployed Cloud Functions.

httpsCallable(): A method provided by the Firebase Functions SDK that creates a callable function reference. This reference can be invoked from your client-side application to execute a Cloud Function that has been configured as an HTTPS Callable Function. It handles the communication details between the client and the Cloud Function.

Asynchronous Task/Operation: A process that does not block the main thread of execution and allows the program to continue running while waiting for the task to complete. Calling a Cloud Function is an asynchronous operation because it involves network requests and processing that takes time.

Callback Function: A function that is passed as an argument to another function and is executed at a later point in time, often after an asynchronous operation has completed. In the .then() and .catch() blocks, the provided functions are callback functions that are executed when the Promise resolves or rejects, respectively.

In this code:

  1. const addAdminRole = functions.httpsCallable('addAdminRole'); creates a callable function reference named addAdminRole. 'addAdminRole' must match the exact name of your deployed Cloud Function.
  2. addAdminRole({ email: adminEmail }) invokes the Cloud Function. We pass an object { email: adminEmail } as data to the function. This data is accessible in the Cloud Function’s data parameter.
  3. .then((result) => { ... }) is a promise-based approach to handle the successful execution of the Cloud Function. When the function call is successful, the code inside the .then() block is executed. result contains the response from the Cloud Function. We access the returned message using result.data.message.
  4. .catch((error) => { ... }) handles any errors that occur during the Cloud Function call. If there is an error, the code inside the .catch() block is executed, and error contains details about the error.

By logging result.data.message to the console, we can display the success message returned by our Cloud Function, confirming that the user has been successfully made an admin. Error handling is also included using .catch() to log any errors that might occur during the function call.

Conclusion

This chapter demonstrated how to call a Cloud Function from your web application’s frontend using Firebase. By setting up a simple HTML form, initializing the Firebase Functions SDK, and writing JavaScript code to handle form submission and invoke the Cloud Function using httpsCallable, you can seamlessly integrate backend logic into your frontend application, creating a more dynamic and feature-rich user experience. In the next steps, you might explore how to further enhance this functionality by implementing user authentication checks and more sophisticated error handling and user feedback mechanisms.


Implementing Role-Based Access Control on the Frontend Using Custom Claims

This chapter will guide you through the process of implementing role-based access control on the frontend of your application using Firebase Custom Claims. Building upon the previous chapter where we learned how to assign admin roles to users via a Cloud Function, we will now focus on how to leverage these custom claims to dynamically alter the user interface (UI) and user experience based on their assigned roles. Specifically, we will demonstrate how to show or hide certain UI elements, such as administrative actions and links, depending on whether a user has the ‘admin’ custom claim set to true.

Retrieving and Utilizing Custom Claims on the Frontend

In the preceding chapter, we successfully implemented a Cloud Function to assign the ‘admin’ custom claim to a user. This claim acts as a flag, indicating whether a user possesses administrative privileges. Now, our objective is to access and utilize this claim within our frontend application to control UI elements and features.

Modifying HTML to Identify Admin-Specific Elements

The first step in implementing role-based access control is to identify the elements in our HTML that should only be visible to administrators. We will achieve this by adding a specific class to these elements.

  • Identify Admin Elements: In your index.html file, locate the HTML elements that should be restricted to admin users. For instance, elements related to creating guides or accessing admin actions.

  • Add the ‘admin’ Class: Assign the class admin to each of these identified elements. For example, consider the following modifications:

    <a href="#" class="admin">Create Guide</a>
    <div class="admin admin-actions">
        <!-- Admin actions form content -->
    </div>
  • Initial Hiding of Admin Elements: To ensure that these elements are hidden by default for non-admin users, we will initially set their display style property to none in our CSS or inline styles. This ensures that until we determine the user’s role, these elements remain hidden.

    <a href="#" class="admin" style="display: none;">Create Guide</a>
    <div class="admin admin-actions" style="display: none;">
        <!-- Admin actions form content -->
    </div>

Accessing Custom Claims After User Login

To determine if a logged-in user has the ‘admin’ custom claim, we need to access their ID token and retrieve the claims. Firebase provides a method to achieve this.

  • getIDTokenResult() Method: Within our authentication state change listener (typically in a JavaScript file handling user authentication, like ar.js in the transcript example), we need to retrieve the ID token result when a user successfully logs in. The getIDTokenResult() method is used for this purpose.

    getIDTokenResult(): This is an asynchronous method provided by Firebase Authentication on the User object. It retrieves the ID token for the user, which contains standard claims as well as any custom claims that have been set. It returns a Promise that resolves with an IDTokenResult object.

  • Asynchronous Operation and Promises: The getIDTokenResult() method is asynchronous, meaning it performs its operation in the background without blocking the main thread. It returns a Promise, which represents the eventual result of the asynchronous operation. We use .then() to handle the resolved result.

    firebase.auth().onAuthStateChanged(user => {
        if (user) {
            user.getIdTokenResult().then(idTokenResult => {
                // Access claims from idTokenResult
                console.log(idTokenResult.claims);
            });
        } else {
            // User is logged out
        }
    });
  • Accessing Claims: The getIDTokenResult() method, upon successful completion, provides an IDTokenResult object. This object contains a claims property, which is an object containing all the claims associated with the user, including our custom ‘admin’ claim.

    user.getIdTokenResult().then(idTokenResult => {
        console.log(idTokenResult.claims); // Output the claims object to the console
        console.log(idTokenResult.claims.admin); // Access the 'admin' claim specifically
    });

Attaching the Admin Claim to the User Object

For easier access and utilization throughout our application, it’s beneficial to attach the ‘admin’ claim directly to the user object after retrieving it.

  • Extending the User Object: Within the .then() block of getIDTokenResult(), we can augment the user object by adding a new property, for example, admin, and assign it the value of the ‘admin’ claim from the idTokenResult.claims.

    user.getIdTokenResult().then(idTokenResult => {
        user.admin = idTokenResult.claims.admin; // Attach the 'admin' claim to the user object
        // ... rest of your logic ...
    });

    If the ‘admin’ claim is not present (e.g., for non-admin users), idTokenResult.claims.admin will likely be undefined or null, which will be reflected in the user.admin property.

Dynamically Updating the UI Based on Admin Status

Now that we have attached the ‘admin’ status to the user object, we can use this information to dynamically update the UI and show or hide the admin-specific elements.

  • setupUI() Function: In your JavaScript code (like index.js in the transcript example), locate the function responsible for setting up the user interface based on the user’s login state (e.g., setupUI()).

  • Querying Admin Elements: Inside the setupUI() function, obtain a collection of all HTML elements that have the admin class using document.querySelectorAll('.admin').

    document.querySelectorAll(): This JavaScript method is used to select all elements in the document that match a specified CSS selector. It returns a NodeList, which is an array-like collection of the matching elements.

    const adminItems = document.querySelectorAll('.admin');
  • Conditional Display Logic: Within the logic of your setupUI() function, after checking if a user is logged in and has the user.admin property set to true, iterate through the adminItems NodeList and set the style.display property of each item to block to make them visible.

    function setupUI(user) {
        if (user) {
            if (user.admin) {
                adminItems.forEach(item => item.style.display = 'block'); // Show admin elements
            } else {
                adminItems.forEach(item => item.style.display = 'none'); // Hide admin elements for non-admins
            }
            // ... other UI setup for logged-in users ...
        } else {
            adminItems.forEach(item => item.style.display = 'none'); // Hide admin elements when logged out
            // ... UI setup for logged-out users ...
        }
    }
  • Hiding Admin Elements for Non-Admins and Logged-Out Users: Ensure that within the else blocks (when user.admin is not true or when the user is logged out), you iterate through the adminItems and set their style.display to none to hide them.

Visual Admin Status Indicator

To provide users with clear feedback about their admin status, consider adding a visual indicator to the UI.

  • Conditional Rendering in HTML: Within your HTML, add a section (e.g., within an account details area) that conditionally displays text indicating whether the user is an admin. This can be achieved using JavaScript to dynamically update the content of an element based on the user.admin property.

  • Ternary Operator for Concise Logic: A ternary operator provides a concise way to conditionally render content.

    Ternary Operator: A shorthand for an if-else statement in many programming languages, including JavaScript. It takes three operands: a condition followed by a question mark (?), an expression to execute if the condition is truthy, followed by a colon (:), and finally an expression to execute if the condition is falsy.

    <div class="account-details">
        <!-- ... other account details ... -->
        <div class="admin-status pink-text">
            ${user.admin ? 'Admin' : ''}
        </div>
    </div>

    In this example, if user.admin is true, the string ‘Admin’ will be displayed; otherwise, an empty string will be displayed, effectively hiding the indicator for non-admin users. The pink-text class is used for styling to visually differentiate the admin status.

Testing and Verification

After implementing these steps, it is crucial to thoroughly test the role-based access control to ensure it functions as expected.

  • Test with Admin User: Log in with a user account that has been assigned the ‘admin’ custom claim. Verify that the admin-specific UI elements (e.g., “Create Guide” link, admin actions panel) are visible and that the admin status indicator is displayed.

  • Test with Non-Admin User: Log in with a user account that does not have the ‘admin’ custom claim. Confirm that the admin-specific UI elements are hidden and that the admin status indicator is not displayed.

  • Test Role Changes: If you modify a user’s admin status (e.g., using the Cloud Function from the previous chapter), log out and log back in with that user. Verify that the UI updates correctly to reflect the changed admin status.

Security Considerations and Next Steps

While this chapter demonstrates how to control UI elements on the frontend based on custom claims, it’s essential to understand that this approach alone does not provide robust security. A user with malicious intent could potentially bypass frontend UI restrictions by inspecting the HTML and manually changing the display property of elements.

Important Security Note: Frontend role-based UI control is primarily for user experience and should not be relied upon for critical security measures.

For true security, you must implement server-side security rules, such as Firebase Security Rules for Firestore or Realtime Database, and security checks within your backend functions (e.g., Cloud Functions).

Next Steps: The subsequent chapter will address the crucial aspect of securing data access using Firebase Security Rules. We will explore how to configure Firestore rules to ensure that only users with the ‘admin’ custom claim can perform administrative actions, thus providing a complete and secure role-based access control system.


Securing Data with Firestore Rules and Custom Claims: A Deep Dive into Backend Security

This chapter explores the crucial aspect of backend security in web applications, focusing on how to restrict data access using Firebase Firestore rules and custom claims. We will delve into a practical example of securing a “guides” collection, ensuring that only administrators have the authority to create new guides, while all authenticated users can read them.

The Importance of Backend Data Security

In web development, it’s common to control user interface (UI) elements based on user roles. For instance, you might hide administrative functionalities from regular users in the front end. However, relying solely on front-end UI manipulation for security is insufficient. A determined user with technical knowledge can bypass these front-end restrictions.

Front End: The part of a website or application that users directly interact with. This includes the user interface (UI) and user experience (UX) elements displayed in a web browser or app.

As demonstrated in the transcript, even if a “Create Guide” link is hidden from a non-admin user in the UI, a savvy developer can use browser developer tools to inspect the page’s code, locate the hidden element, and manually make it visible. This action, however, only circumvents the visual restriction. The underlying data and backend logic remain vulnerable if not properly secured.

Therefore, true security must be enforced at the backend level, where data is stored and accessed. Firebase Firestore, a NoSQL document database, provides a powerful mechanism for defining these backend security rules.

Backend: The server-side of a web application, responsible for data storage, processing, and business logic. It operates behind the scenes and is not directly accessed by users.

Firestore Rules: The Foundation of Backend Security

Firestore Rules act as a security gatekeeper, controlling access to your database at a granular level. They define conditions that must be met for read and write operations to be allowed. These rules are evaluated by Firebase servers before any data access is granted, ensuring robust and reliable security.

Firestore Rules: A declarative language used to define how data in your Firebase Firestore database can be accessed, based on user authentication and data structure. These rules are enforced on the server-side.

In this context, the goal is to secure the “guides” collection in Firestore so that:

  • Reading Guides: All authenticated users can read existing guides.
  • Writing Guides (Creating New Guides): Only users with administrative privileges can create new guides.

To achieve this, we will leverage Firebase Custom Claims.

Custom Claims: Defining User Roles

Custom claims are a powerful feature of Firebase Authentication that allows you to assign custom attributes or roles to users. These claims are encoded within the user’s authentication token and can be accessed in Firestore rules and application code.

Custom Claims: Custom attributes that can be set on a Firebase user’s authentication token. These claims can be used to implement role-based access control and other authorization logic in your application and Firestore rules.

In our scenario, we use a custom claim named “admin.” Users who are designated as administrators will have the “admin” claim set to true in their authentication token. This claim will then be used in Firestore rules to control write access to the “guides” collection.

Implementing Firestore Rules for Secure Guide Creation

Let’s examine the Firestore rules implemented in the transcript to secure the “guides” collection.

Initially, the read rule is set to allow any authenticated user to read guides:

allow read: if request.auth.uid != null;

request.auth.uid: A variable within Firestore Rules that represents the unique user ID (UID) of the user making the request. If a user is authenticated, this variable will contain their UID; otherwise, it will be null.

This rule states: “Allow read if the user is authenticated (i.e., request.auth.uid is not null).” This ensures that only logged-in users can access the guides data for reading.

To restrict write access to only administrators, the following rule is added:

allow write: if request.auth.token.admin == true;

request.auth.token: A variable within Firestore Rules that provides access to the user’s Firebase Authentication token. This token contains standard claims (like UID) as well as any custom claims that have been set.

request.auth.token.admin: Accesses the “admin” custom claim within the user’s authentication token. This will be true if the user has been assigned the “admin” role and false or undefined otherwise.

This rule states: “Allow write if the ‘admin’ claim in the user’s authentication token is equal to true.” This rule, combined with the read rule, effectively secures the “guides” collection:

  • Authenticated users can read guides.
  • Only users with the ‘admin’ custom claim set to true can write (create or modify) guides.

By publishing these rules to Firestore, we enforce backend security for the “guides” collection.

Testing the Security Rules

To verify the effectiveness of these Firestore rules, two scenarios were tested in the transcript:

1. Non-Admin User Attempting to Create a Guide

When a user who is not an administrator attempts to create a new guide, even if they manually make the “Create Guide” UI element visible, the Firestore rules prevent the write operation.

Upon attempting to create a guide, the application displays an error message in the browser’s console: “Missing or insufficient permissions.”

Console (Browser Console): A tool built into web browsers that developers use to log messages, debug code, and inspect the web page. Error messages from backend services, like Firestore, often appear in the console.

Missing or insufficient permissions: A common error message in Firebase and Firestore indicating that the user or application does not have the necessary authorization to perform the requested operation due to security rules.

This error confirms that the Firestore rules are working as expected and blocking unauthorized write access from non-admin users, regardless of UI manipulations.

2. Admin User Attempting to Create a Guide

Conversely, when an administrator user (identified by having the “admin” custom claim set to true) attempts to create a guide, the operation is successful. No “missing or insufficient permissions” error is encountered, and the new guide is successfully added to the Firestore “guides” collection.

This successful creation demonstrates that the Firestore rules correctly allow write access for authorized administrator users.

Conclusion: Robust Security Through Backend Enforcement

This example highlights the critical importance of implementing backend security rules, particularly when dealing with sensitive data or functionalities that should be restricted to specific user roles. Simply hiding UI elements is not a sufficient security measure.

By utilizing Firebase Firestore rules and custom claims, we can effectively enforce robust and granular access control at the backend level. This approach ensures that data is protected, and only authorized users can perform specific operations, regardless of any front-end manipulations. Backend security rules are the cornerstone of building secure and reliable web applications.


Securing Cloud Functions: Restricting Admin Actions to Authorized Users

This chapter focuses on enhancing the security of cloud functions, specifically addressing the scenario where administrative actions, such as adding new administrators, should be restricted to only authorized admin users. We will explore how to implement server-side validation within a Firebase Cloud Function to ensure that only legitimate administrators can perform these privileged operations.

The Security Challenge: Preventing Unauthorized Admin Modifications

In the previous step, we successfully implemented a security rule to control data access, ensuring that only administrators could write new guides. However, a vulnerability remains in the application’s admin management feature. Consider the scenario where a non-admin user, such as “Mario,” logs into the application. Even though the user interface may hide the “add admin” functionality from non-admins, a technically proficient user could potentially bypass this client-side restriction.

As demonstrated in the transcript, a user can utilize browser developer tools, specifically the “inspect element” feature, to reveal hidden elements on a webpage.

Inspect element: A feature found in web browser developer tools that allows users to examine the HTML structure, CSS styles, and JavaScript code of a webpage. It can be used to view and modify the client-side code of a website directly in the browser.

By using “inspect element,” a malicious user could:

  • Locate the hidden form or button responsible for adding new administrators.
  • Modify the HTML code to make this hidden element visible on the page.
  • Interact with the now-visible element to attempt to add themselves or others as administrators.

This client-side manipulation highlights the critical need for server-side security measures. Relying solely on hiding elements in the user interface is insufficient to secure sensitive functionalities. We must implement checks within our Cloud Functions, the server-side code, to verify the user’s authorization before executing administrative actions.

Cloud Function: Server-side code that responds to events triggered by Firebase features and HTTPS requests. Cloud Functions are written in Node.js or Python and run in a serverless environment, automatically scaling to handle demand. They are used to implement backend logic and extend Firebase functionality.

Implementing Server-Side Admin Role Validation

To address the security vulnerability, we will modify the Cloud Function responsible for adding new administrators. The goal is to ensure that this function only executes successfully when called by a user who is already authenticated as an administrator.

We will leverage the context object provided by Firebase when a Cloud Function is invoked.

Context Object: An object automatically passed to a Cloud Function when it is triggered. It contains information about the event that triggered the function and the execution environment, including details about the authenticated user making the request (if applicable).

This context object contains authentication information about the user making the request, accessible through context.auth. Specifically, we can access the user’s auth token and its associated claims, including the custom claim we previously set up to identify administrators.

Auth Token: A security token issued by Firebase Authentication after a user successfully signs in. This token securely verifies the user’s identity and can be used to access protected resources and services, including Cloud Functions. It can also contain custom claims that define user roles or permissions.

Within our Cloud Function, we will implement the following logic:

  1. Check Admin Status: Retrieve the admin status from the user’s auth token within the context object. This is accessed via context.auth.token.admin.
  2. Conditional Execution: If the context.auth.token.admin value is not equal to true (meaning the user is not an admin), the function will immediately return an error.
  3. Error Response: The function will return a specific error message to the client, indicating that only administrators are authorized to perform this action.
  4. Authorized Execution: If the context.auth.token.admin value is true, the function will proceed with its original logic of adding a new administrator.

Here’s how this logic is implemented in code (as seen in the transcript):

exports.addAdminRole = functions.https.onCall((data, context) => {
  // ... (previous code for setting custom claim) ...

  // Check if request is made by an admin
  if (context.auth.token.admin !== true) {
    // Return an error if not an admin
    return {
      error: 'Only admins can add other admins sucker'
    };
  }

  // ... (rest of the function code to set custom claim - only executed if admin) ...
});

In this code snippet:

  • context.auth.token.admin retrieves the value of the admin custom claim from the user’s auth token.
  • if (context.auth.token.admin !== true) checks if the user is not an admin.
  • return { error: 'Only admins can add other admins sucker' }; returns an error object, preventing the rest of the function from executing for non-admin users.

Deploying and Testing the Secure Cloud Function

After implementing the admin role validation, the next crucial step is to deploy the updated Cloud Function to Firebase.

Deploy: The process of uploading and activating code or configurations to a server or cloud environment, making the changes live and accessible. In Firebase, deploying functions makes the updated code live and executable in the Firebase project.

The transcript mentions using the Firebase CLI (Command Line Interface) command:

firebase deploy --only functions

This command specifically deploys only the functions within your Firebase project, ensuring that the latest code is running in the cloud environment.

After deployment, it’s essential to test the security implementation thoroughly. The transcript demonstrates two key test cases:

  1. Testing as a Non-Admin User (Mario):

    • Log in to the application as a non-admin user (e.g., “Mario”).

    • Use “inspect element” to reveal the hidden “add admin” form.

    • Attempt to add themselves as an administrator.

    • Observe the console in the browser’s developer tools for error messages.

    Console (Browser Console): A feature in web browser developer tools that displays logged messages, errors, and warnings from JavaScript code running on a webpage. It is a valuable tool for debugging and monitoring client-side web applications.

    The expected outcome is that the Cloud Function should return the custom error message “Only admins can add other admins sucker,” and the operation should fail. This confirms that the server-side validation is working correctly and preventing unauthorized admin additions.

  2. Testing as an Admin User (Sean):

    • Log in to the application as an admin user (e.g., “Sean”).
    • Use the legitimate “add admin” functionality (or “inspect element” if necessary to ensure the function is reachable).
    • Attempt to add a new administrator (e.g., “Mario”).
    • Verify that the operation succeeds and the new user is successfully added as an administrator.

This test confirms that authorized administrators can still perform the intended admin actions, while the security restrictions are effectively applied to non-admin users.

Conclusion: Server-Side Validation for Robust Security

This chapter demonstrates the importance of server-side validation in securing web applications.

Server-side validation: The process of verifying user input and enforcing security rules on the server, rather than solely relying on client-side checks. Server-side validation is crucial for security because it cannot be bypassed by malicious users manipulating client-side code.

While client-side UI elements can provide a degree of user experience control, they are not sufficient for security. By implementing validation within Cloud Functions, we ensure that security rules are enforced on the server, where they cannot be bypassed by client-side manipulations. This approach provides a more robust and reliable security model for sensitive operations like admin management.

The next step, as mentioned in the transcript, will focus on enhancing user experience by implementing proper error handling for user authentication processes, such as sign-up and login, providing informative feedback to users in case of issues like password mismatches or invalid email formats.


Enhancing User Experience with Error Handling in Login and Signup Forms

This chapter will guide you through the process of implementing error handling in login and signup forms to improve user experience. By displaying clear and informative error messages, you can help users understand and rectify mistakes they make during the authentication process. This prevents user frustration and ensures a smoother interaction with your application.

Introduction to Error Handling in User Authentication

When users interact with login and signup forms, they might make errors such as entering incorrect passwords, using invalid email formats, or providing passwords that don’t meet complexity requirements. Without proper error handling, users may encounter a frustrating experience where the application appears to hang or fail silently. Implementing error handling allows you to catch these issues and provide immediate feedback to the user, guiding them to correct their input and successfully complete the login or signup process.

This chapter focuses on enhancing the user interface (UI) by displaying error messages directly within the forms, specifically within modal windows. We will cover the steps involved in modifying the HTML structure and JavaScript code to achieve this functionality.

HTML (HyperText Markup Language)

The standard markup language for documents designed to be displayed in a web browser. It can be assisted by technologies such as Cascading Style Sheets (CSS) and scripting languages such as JavaScript.

Modal Window (or Modal)

A window that forces the user to interact with it before they can return to operating the parent application. Modals are often used to display important information or require user input before proceeding.

Implementing Error Display in Signup Form

To begin, we will focus on implementing error display in the signup form. This involves making changes to both the HTML structure of the signup modal and the JavaScript code responsible for user signup.

Modifying the Signup Modal HTML

First, we need to add a designated area within the signup modal to display error messages. This is achieved by adding an HTML element, specifically a <p> (paragraph) tag, within the signup modal’s HTML structure.

  1. Locate the Signup Modal: Access the HTML code for your signup modal.

  2. Insert Error Placeholder: Insert the following HTML code snippet just below the signup button within the modal.

    <p class="error pink-text center-align"></p>

    This code snippet introduces a paragraph element with the following attributes:

    • class="error": This assigns the CSS class “error” to the paragraph, allowing us to target this element specifically using JavaScript and apply styling for error messages.

      Class (in HTML)

      An attribute that specifies one or more class names for an HTML element. Classes are primarily used to allow CSS and JavaScript to select and access specific elements via the class names.

    • pink-text: This class (presumably defined in your CSS stylesheet) styles the text color to pink, visually distinguishing error messages.

    • center-align: This class (likely also defined in your CSS) centers the text within the paragraph element.

    Initially, this paragraph tag is empty as there are no errors to display when the modal first loads. It serves as a placeholder that will be dynamically populated with error messages when necessary.

Updating JavaScript for Signup Error Handling

Next, we need to modify the JavaScript code that handles the signup process to detect errors and display them in the newly created error placeholder.

  1. Locate Signup Function: Find the JavaScript function responsible for handling user signup. This function typically sends a request to a backend service to create a new user account.

  2. Implement Error Catching: Within the signup function, after the code that attempts to sign up the user, add a .catch() method. This method is used to handle any errors that occur during the signup process.

    Catch Method (in JavaScript Promises)

    A method used with Promises in JavaScript for error handling. It is chained to a Promise and executes a provided function if the Promise is rejected, allowing you to handle errors that occurred during asynchronous operations.

    // ... (Signup code that attempts to sign up the user) ...
    .catch(error => {
        // Error handling code will go here
    });
  3. Display Error Message: Inside the .catch() method’s callback function (the function that executes when an error is caught), write the code to display the error message in the signup modal’s error paragraph.

    Callback Function

    A function passed as an argument to another function, to be executed at a later time. In asynchronous operations, like network requests, callback functions are often used to handle the response or error when the operation completes.

    .catch(error => {
        const signupForm = document.querySelector('.signup-form'); // Select the signup form
        const errorParagraph = signupForm.querySelector('.error'); // Select the error paragraph within the signup form
        errorParagraph.innerHTML = error.message; // Set the inner HTML of the paragraph to the error message
    });

    Let’s break down this code:

    • document.querySelector('.signup-form'): This line uses a querySelector to select the HTML element representing the signup form. It assumes your signup form has a class of “signup-form”.

      Query Selector (in JavaScript)

      A method of the Document interface which returns the first element within the document that matches the specified selector, or group of selectors. It allows you to select HTML elements based on CSS-style selectors like classes, IDs, and tag names.

    • signupForm.querySelector('.error'): This line further uses querySelector, but this time it’s called on the signupForm element. This limits the search to elements within the signup form, and it selects the paragraph tag with the class “error” that we added earlier.

    • errorParagraph.innerHTML = error.message;: This line sets the innerHTML property of the errorParagraph element. innerHTML allows you to set or get the HTML content within an element. Here, it is being set to error.message. It’s assumed that the error object caught by the .catch() method has a message property containing a user-friendly error description.

      Inner HTML (in JavaScript)

      A property that gets or sets the HTML markup contained within an element. Setting innerHTML replaces the existing content of the element with the provided HTML string.

  4. Clear Error Message on Success/Correction: After a user successfully signs up, or if they correct their input after an error and attempt signup again, you should clear any previously displayed error message. This can be done by setting the innerHTML of the error paragraph to an empty string (""). This should be placed in the code path that executes when the signup is successful, and also potentially at the beginning of the signup function to clear any prior errors before a new attempt.

    // Example of clearing the error message on successful signup (or at the start of the signup function):
    const signupForm = document.querySelector('.signup-form');
    const errorParagraph = signupForm.querySelector('.error');
    errorParagraph.innerHTML = ""; // Clear the error message
    // ... (Rest of your signup code) ...

Testing the Signup Error Handling

To test the implementation, try signing up with invalid data, such as a password that is too short. If configured correctly, you should see the error message “the password should be at least 6 characters” (or a similar relevant message based on your validation rules) displayed in the error paragraph within the signup modal. After correcting the error and signing up successfully, the error message should disappear, and the modal should behave as expected.

Implementing Error Display in Login Form

The process for implementing error display in the login form is very similar to that of the signup form.

Modifying the Login Modal HTML

  1. Locate Login Modal: Access the HTML code for your login modal.

  2. Insert Error Placeholder: Just as with the signup modal, insert the same error placeholder paragraph tag below the login button in the login modal’s HTML.

    <p class="error pink-text center-align"></p>

Updating JavaScript for Login Error Handling

  1. Locate Login Function: Find the JavaScript function handling user login.

  2. Implement Error Catching: Add a .catch() method to the login function, similar to the signup function.

    // ... (Login code that attempts to log in the user) ...
    .catch(error => {
        // Error handling code for login will go here
    });
  3. Display Error Message (Login): Within the .catch() callback function for login, use similar code to display the error message in the login modal’s error paragraph. The key difference is to ensure you are selecting elements within the login form (.login-form) instead of the signup form.

    .catch(error => {
        const loginForm = document.querySelector('.login-form'); // Select the login form
        const errorParagraph = loginForm.querySelector('.error'); // Select the error paragraph within the login form
        errorParagraph.innerHTML = error.message; // Set the inner HTML to the error message
    });
  4. Clear Error Message on Success/Correction (Login): Just as with signup, clear the error message when login is successful or when the user corrects their input and attempts to log in again.

    // Example of clearing the error message on successful login (or at the start of the login function):
    const loginForm = document.querySelector('.login-form');
    const errorParagraph = loginForm.querySelector('.error');
    errorParagraph.innerHTML = ""; // Clear the error message
    // ... (Rest of your login code) ...

Testing the Login Error Handling

Test the login error handling by attempting to log in with incorrect credentials. Try scenarios like:

  • Non-existent user: Enter an email address that is not registered. You should see an error message like “There is no user record corresponding to this identifier.”
  • Incorrect password: Enter the correct email address but an incorrect password. You should see an error message like “invalid password”.
  • Correct credentials: Verify that when you enter correct login details, the login is successful, and any error messages are cleared.

Conclusion

By implementing these steps, you have successfully added error handling to both your signup and login forms. This enhancement significantly improves the user experience by providing immediate and actionable feedback when users encounter issues during authentication. Displaying error messages directly within the forms makes it clear to users what went wrong and how to correct it, leading to a more intuitive and user-friendly application. This simple yet effective technique is crucial for creating robust and user-centric web applications.


Chapter: Concluding Our Firebase Authentication Journey: Next Steps and Further Learning

Introduction

This chapter marks the conclusion of our exploration into Firebase Authentication, specifically focusing on implementation using email addresses and passwords. We sincerely hope this journey has been both enjoyable and educational, providing you with valuable knowledge and skills in this crucial area of web and application development. While we have covered significant ground, it’s important to recognize that this is just the beginning of what Firebase Authentication offers.

Core Concepts Covered: Email and Password Authentication

Throughout this series, we have concentrated on the fundamental method of user authentication using email addresses and passwords. This approach is a cornerstone of many applications, providing a familiar and reliable way for users to create accounts and securely access services. We have delved into the practical steps and considerations involved in implementing this authentication method within the Firebase ecosystem.

Expanding Your Authentication Horizons: Exploring Other Firebase Methods

While email and password authentication is widely used, Firebase provides a diverse range of authentication providers to cater to various application needs and user preferences. These methods extend beyond the scope of this series but are readily available within the Firebase platform.

Firebase: A mobile and web application development platform offered by Google. It provides tools and services for building, improving, and growing apps.

As you progress in your Firebase journey, it’s crucial to be aware of these alternative authentication options. They offer flexibility and can enhance the user experience by providing convenient login methods. To delve deeper into these methods, we strongly recommend consulting the official Firebase documentation. The implementation of these alternative methods is often straightforward once you have grasped the foundational concepts of authentication, which we have covered in detail.

Authentication: The process of verifying the identity of a user, device, or other entity. It ensures that users are who they claim to be before granting access to resources or services.

Should there be sufficient interest, we are open to exploring these additional authentication methods in future content. Your feedback and requests are invaluable in shaping future learning resources, so please feel free to express your interest in the comments section.

Venturing Beyond Authentication: Exploring Firestore and Real-time Databases

Our series has implicitly utilized Firebase’s Realtime Database and likely Firestore to support authentication functionalities. However, these databases are powerful tools in their own right, offering robust solutions for data storage and synchronization.

Firestore: A flexible, scalable NoSQL cloud database for mobile, web, and server development from Firebase and Google Cloud Platform. It is designed for storing and syncing data at scale.

Real-time Database: A cloud-hosted NoSQL database that lets you store and synchronize data between users in real-time. Changes to data are instantly reflected across connected devices.

For those eager to deepen their understanding of these databases, a dedicated series focusing on Firestore and Real-time Database is available. This resource will provide a comprehensive exploration of their features, capabilities, and practical applications. We encourage you to explore this series to expand your skillset beyond authentication and into the realm of data management with Firebase.

Integrating Firebase with Other Technologies: Vue.js and Beyond

To further illustrate the versatility of Firebase, consider its integration with other front-end technologies. One such example is Vue.js, a progressive JavaScript framework for building user interfaces.

Vue.js: A progressive JavaScript framework for building user interfaces. It is designed to be incrementally adoptable and focuses on the view layer.

A comprehensive Udemy course is available that demonstrates the practical application of Firebase with Vue.js. This course involves building three web applications and showcases the integration of Firestore, Cloud Functions, and authentication within a Vue.js environment.

Cloud Functions: Serverless functions that let you run backend code in response to events triggered by Firebase features and HTTPS requests. They allow you to extend Firebase functionality without managing servers.

Udemy: An online learning and teaching marketplace. It offers a wide range of courses, often at discounted prices.

This course provides a valuable opportunity to see Firebase in action within a different technological context and further solidify your understanding of its capabilities. A discounted price is available through a coupon provided in the description, making this resource even more accessible.

Conclusion and Next Steps

We hope this Firebase Authentication series has been a worthwhile investment of your time and has equipped you with the knowledge and skills to implement secure authentication in your projects. Remember that learning is a continuous process, and there is always more to explore within the Firebase ecosystem.

To continue your learning journey, we encourage you to:

  • Explore the Firebase Documentation: The official documentation is an invaluable resource for in-depth information on all Firebase features, including the alternative authentication methods mentioned earlier.
  • Consider the Firestore and Real-time Database Series: Expand your backend knowledge by delving into Firebase’s database solutions.
  • Investigate the Vue.js and Firebase Udemy Course: See Firebase integration in action with a popular front-end framework.
  • Engage and Share: If you found this series helpful, please consider sharing it with others who might benefit. Your support through likes and subscriptions is greatly appreciated and encourages the creation of future educational content.

Thank you for joining us on this Firebase Authentication journey. We look forward to seeing you in the next series!