0% found this document useful (0 votes)
2 views96 pages

Angular19 Firebase Guide

This guide provides a comprehensive approach to integrating Firebase services with Angular 19 standalone components, aimed at mid-level developers. It covers building a To-Do application that utilizes Firebase Authentication, Firestore, Storage, Cloud Functions, and Hosting, along with deployment using GitLab CI/CD. The document includes prerequisites, project setup, Firebase project creation, and detailed instructions for implementing authentication and other features.

Uploaded by

Ahmed Yassin
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views96 pages

Angular19 Firebase Guide

This guide provides a comprehensive approach to integrating Firebase services with Angular 19 standalone components, aimed at mid-level developers. It covers building a To-Do application that utilizes Firebase Authentication, Firestore, Storage, Cloud Functions, and Hosting, along with deployment using GitLab CI/CD. The document includes prerequisites, project setup, Firebase project creation, and detailed instructions for implementing authentication and other features.

Uploaded by

Ahmed Yassin
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Angular 19 + Firebase Integration Guide

1. Introduction
Welcome to this comprehensive guide on integrating Firebase services with Angular 19
standalone components. This guide is designed for mid-level Angular developers who
want to leverage Firebase's powerful backend services in their applications.

In this guide, we'll build a practical To-Do application that demonstrates how to
integrate and use the following Firebase services:

• Firebase Authentication: Secure user authentication and session management


• Firestore Database: Real-time NoSQL database for storing and syncing data
• Firebase Storage: File storage for attachments and media
• Firebase Cloud Functions: Serverless functions for backend logic
• Firebase Hosting: Deployment and hosting of the application

We'll also cover deployment using GitLab CI/CD pipelines, ensuring you have a complete
end-to-end solution from development to production.

Why Angular 19 Standalone Components?

Angular 19 introduced significant improvements to the standalone components API,


making it easier to build modular, maintainable applications without the traditional
NgModule system. Standalone components offer:

• Simplified dependency management


• Improved tree-shaking
• Better developer experience
• More intuitive component composition

Why Firebase?

Firebase provides a comprehensive suite of backend services that allow developers to


focus on building great user experiences without managing server infrastructure. Key
benefits include:

• Real-time data synchronization


• Built-in authentication
• Scalable hosting
• Serverless functions
• Integrated storage solutions
• Comprehensive security rules

By the end of this guide, you'll have a fully functional To-Do application with user
authentication, real-time data synchronization, file storage capabilities, and serverless
backend functions—all deployed through an automated CI/CD pipeline.

Let's get started!

2. Prerequisites
Before we begin building our Angular 19 + Firebase To-Do application, let's ensure you
have all the necessary tools and accounts set up.

Required Software

# Check [Link] version (v16.x or higher recommended)


node -v

# Check npm version (v8.x or higher recommended)


npm -v

# Install Angular CLI globally


npm install -g @angular/cli

# Check Angular CLI version


ng version

# Install Firebase CLI globally


npm install -g firebase-tools

# Check Firebase CLI version


firebase --version

After running these commands, you should have: - [Link] v16.x or higher - npm v8.x or
higher - Angular CLI v16.x or higher (compatible with Angular 19) - Firebase CLI v12.x or
higher

These tools are essential for developing and deploying our Angular application with
Firebase integration.
Account Requirements
1. Firebase Account: You'll need a Google account to access Firebase. If you don't
have a Firebase project yet, we'll create one in the next section.

2. GitLab Account: For CI/CD deployment, you'll need a GitLab account. Make sure
you have access to create repositories and configure CI/CD pipelines.

IDE Recommendations
While you can use any code editor, Visual Studio Code provides excellent support for
Angular and TypeScript development. Consider installing these extensions:

• Angular Language Service


• ESLint
• Firebase Explorer
• GitLens

Project Planning
Before diving into code, let's understand what we'll be building:

• A To-Do application with user authentication


• CRUD operations for to-do items using Firestore
• Ability to attach files to to-do items using Firebase Storage
• Background processing with Firebase Cloud Functions
• Automated deployment using GitLab CI/CD

Now that we have our prerequisites covered, let's move on to setting up our Angular 19
project with standalone components.

3. Project Setup
In this section, we'll set up a new Angular 19 project using standalone components and
prepare it for Firebase integration.

Creating a New Angular Project

# Create a new Angular project with standalone components


ng new angular-firebase-todo --standalone --routing --style=scss
# Navigate to the project directory
cd angular-firebase-todo

# Verify the project structure


ls -la

This command creates a new Angular project with: - Standalone components (no
NgModules) - Routing configuration - SCSS for styling

The --standalone flag is crucial as it configures the project to use Angular's


standalone components architecture instead of the traditional NgModule system.

Project Structure Overview


After creating the project, you should have a structure similar to this:

angular-firebase-todo/
├── .angular/
├── .vscode/
├── node_modules/
├── src/
│ ├── app/
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── ...
│ ├── assets/
│ ├── environments/
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
├── .editorconfig
├── .gitignore
├── [Link]
├── [Link]
├── [Link]
├── [Link]
├── [Link]
└── [Link]
The key files to note are: - [Link] : Contains application-level providers -
[Link] : Defines the application routes - [Link] : The entry point that
bootstraps the application

Installing Firebase Dependencies


Now, let's add the Firebase packages to our project:

# Install Firebase and Angular Fire


npm install firebase @angular/fire

# Optional: Install RxFire for reactive Firebase bindings


npm install rxfire

These packages provide: - firebase : The core Firebase SDK - @angular/fire :


Angular specific bindings for Firebase - rxfire : RxJS utilities for Firebase (optional but
recommended)

Setting Up Environment Configuration


Let's create environment files to store our Firebase configuration:

# Create environments directory if it doesn't exist


mkdir -p src/environments

# Create environment files


touch src/environments/[Link]
touch src/environments/[Link]

Now, let's update the environment files:

// src/environments/[Link]
export const environment = {
production: true,
firebase: {
apiKey: "YOUR_API_KEY",
authDomain: "[Link]",
projectId: "your-project-id",
storageBucket: "[Link]",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID"
}
};

// src/environments/[Link]
export const environment = {
production: false,
firebase: {
apiKey: "YOUR_API_KEY",
authDomain: "[Link]",
projectId: "your-project-id",
storageBucket: "[Link]",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID"
}
};

Important security note: Never commit your actual Firebase API keys to public
repositories. We'll address proper handling of these secrets in the GitLab CI/CD section.
For local development, these placeholder values will be replaced with your actual
Firebase configuration in the next section.

Updating Angular Configuration


Let's update the Angular configuration to use our environment files:

# Update [Link] to include environment files


sed -i 's/"fileReplacements": \[\]/"fileReplacements": \[\
{\
"replace": "src\/environments\/[Link]",\
"with": "src\/environments\/
[Link]"\
}\
\]/g' [Link]

This configuration ensures that during development, Angular will use the development
environment file, while in production, it will use the production environment file.

Now our Angular project is set up and ready for Firebase integration, which we'll cover in
the next section.
4. Firebase Project Setup
In this section, we'll create a Firebase project and configure it for use with our Angular
application.

Creating a Firebase Project


First, let's create a new Firebase project:

# Login to Firebase
firebase login

# Initialize Firebase in your project directory


firebase init

When running firebase init , you'll be prompted to: 1. Select Firebase features
(select Firestore, Hosting, Storage, and Functions) 2. Choose an existing project or create
a new one 3. Accept default options for most prompts, but make sure to: - Choose
TypeScript for Cloud Functions - Use ESLint for Cloud Functions - Don't overwrite
existing files when prompted

This process creates several important files: - [Link] : Configuration for


Firebase services - .firebaserc : Links your local project to your Firebase project -
[Link] : Security rules for Firestore - [Link] : Security rules for
Storage - functions/ : Directory for Cloud Functions

Enabling Required Services


After creating your Firebase project, you need to enable the required services through
the Firebase console:

1. Go to Firebase Console
2. Select your project
3. Navigate to each service and enable it:
4. Authentication: Set up sign-in methods (Email/Password, Google, etc.)
5. Firestore: Create database in production or test mode
6. Storage: Initialize storage with default rules
7. Functions: Will be enabled when you deploy your first function
Getting Firebase Configuration
To connect your Angular app to Firebase, you need the Firebase configuration:

1. In the Firebase console, go to Project Settings


2. Scroll down to "Your apps" section
3. Click the web app icon () to register a new web app if you haven't already
4. Enter a nickname for your app (e.g., "Angular Todo App")
5. Copy the Firebase configuration object

Now, update your environment files with the actual Firebase configuration:

// src/environments/[Link] and
[Link]
export const environment = {
production: true, // or false for development
firebase: {
apiKey: "actual-api-key",
authDomain: "[Link]",
projectId: "your-project-id",
storageBucket: "[Link]",
messagingSenderId: "your-messaging-sender-id",
appId: "your-app-id",
measurementId: "your-measurement-id"
}
};

Securing API Keys


For local development, storing Firebase configuration in environment files is acceptable.
However, for production and CI/CD, we need a more secure approach:

# Create a .env file for local development (add to .gitignore)


echo "FIREBASE_API_KEY=your-api-key
FIREBASE_AUTH_DOMAIN=[Link]
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_STORAGE_BUCKET=[Link]
FIREBASE_MESSAGING_SENDER_ID=your-messaging-sender-id
FIREBASE_APP_ID=your-app-id
FIREBASE_MEASUREMENT_ID=your-measurement-id" > .env

# Add .env to .gitignore


echo ".env" >> .gitignore
Later, we'll use GitLab CI/CD environment variables to securely manage these values in
production.

Integrating Firebase with Angular


Now, let's integrate Firebase with our Angular application by updating the
[Link] file:

// src/app/[Link]
import { ApplicationConfig, importProvidersFrom } from
'@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/
animations';

import { initializeApp, provideFirebaseApp } from '@angular/


fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth';
import { getFirestore, provideFirestore } from '@angular/fire/
firestore';
import { getStorage, provideStorage } from '@angular/fire/
storage';
import { getFunctions, provideFunctions } from '@angular/fire/
functions';
import { getAnalytics, provideAnalytics } from '@angular/fire/
analytics';

import { routes } from './[Link]';


import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {


providers: [
provideRouter(routes),
provideAnimations(),
importProvidersFrom(
provideFirebaseApp(() =>
initializeApp([Link])),
provideAuth(() => getAuth()),
provideFirestore(() => getFirestore()),
provideStorage(() => getStorage()),
provideFunctions(() => getFunctions()),
provideAnalytics(() => getAnalytics())
)
]
};
This configuration: - Initializes Firebase with your configuration - Provides Firebase
services to your Angular application - Uses the standalone components approach with
dependency injection

With this setup, your Angular application is now connected to Firebase and ready to use
its services. In the next sections, we'll implement authentication, Firestore database
operations, storage, and cloud functions.

5. Firebase Authentication
In this section, we'll implement user authentication using Firebase Authentication in our
Angular 19 application.

Setting Up Authentication in Firebase Console


Before writing any code, we need to enable authentication methods in the Firebase
console:

# Open Firebase console in your browser


echo
"Go to [Link] and select your
project"
echo "Navigate to Authentication > Sign-in method and enable
Email/Password authentication"

In the Firebase console: 1. Go to Authentication > Sign-in method 2. Enable Email/


Password authentication 3. Optionally, enable Google, Facebook, or other providers as
needed 4. Save your changes

Creating Authentication Service


Now, let's create an authentication service in our Angular application:

# Create auth directory and service


mkdir -p src/app/auth
touch src/app/auth/[Link]

Implement the authentication service:


// src/app/auth/[Link]
import { Injectable, inject } from '@angular/core';
import {
Auth,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
authState,
UserCredential,
updateProfile,
User
} from '@angular/fire/auth';
import { Observable, from, of, throwError } from 'rxjs';
import { switchMap, catchError, tap } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class AuthService {
private auth: Auth = inject(Auth);

// Observable of the current auth state


currentUser$ = authState([Link]);

// Check if user is logged in


get isLoggedIn$(): Observable<boolean> {
return [Link]$.pipe(
switchMap(user => of(!!user))
);
}

// Register a new user


register(email: string, password: string, displayName:
string): Observable<UserCredential> {
return from(createUserWithEmailAndPassword([Link],
email, password)).pipe(
tap(credentials => {
// Update the user's display name
if ([Link]) {
updateProfile([Link], { displayName });
}
}),
catchError(error => {
[Link]('Registration error:', error);
return throwError(() => new Error(`Registration failed:
${[Link]}`));
})
);
}

// Login with email and password


login(email: string, password: string):
Observable<UserCredential> {
return from(signInWithEmailAndPassword([Link], email,
password)).pipe(
catchError(error => {
[Link]('Login error:', error);
return throwError(() => new Error(`Login failed: $
{[Link]}`));
})
);
}

// Logout the current user


logout(): Observable<void> {
return from(signOut([Link])).pipe(
catchError(error => {
[Link]('Logout error:', error);
return throwError(() => new Error(`Logout failed: $
{[Link]}`));
})
);
}

// Get current user


getCurrentUser(): User | null {
return [Link];
}
}

This service provides: - User registration with email and password - Login with email and
password - Logout functionality - Current user state as an Observable - Error handling for
authentication operations

Note that we're using the functional approach with inject() instead of constructor
injection, which is the recommended pattern for Angular 19 standalone components.

Creating Authentication Components


Now, let's create the necessary components for authentication:

# Create login component


mkdir -p src/app/auth/login
touch src/app/auth/login/[Link]
touch src/app/auth/login/[Link]
touch src/app/auth/login/[Link]

# Create register component


mkdir -p src/app/auth/register
touch src/app/auth/register/[Link]
touch src/app/auth/register/[Link]
touch src/app/auth/register/[Link]

Login Component

// src/app/auth/login/[Link]
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule,
Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../[Link]';

@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class LoginComponent {
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private router = inject(Router);

loginForm: FormGroup = [Link]({


email: ['', [[Link], [Link]]],
password: ['', [[Link],
[Link](6)]]
});

errorMessage: string = '';


loading: boolean = false;

onSubmit(): void {
if ([Link]) {
return;
}

[Link] = true;
[Link] = '';

const { email, password } = [Link];

[Link](email, password).subscribe({
next: () => {
[Link] = false;
[Link](['/todos']);
},
error: (error) => {
[Link] = false;
[Link] = [Link];
}
});
}
}

<!-- src/app/auth/login/[Link] -->


<div class="login-container">
<h2>Login</h2>

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">


<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
formControlName="email"
placeholder="Enter your email"
>
<div class="error" *ngIf="[Link]('email')?.invalid
&& [Link]('email')?.touched">
<span *ngIf="[Link]('email')?.errors?.
['required']">Email is required</span>
<span *ngIf="[Link]('email')?.errors?.['email']">Please
enter a valid email</span>
</div>
</div>

<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
formControlName="password"
placeholder="Enter your password"
>
<div class="error"
*ngIf="[Link]('password')?.invalid &&
[Link]('password')?.touched">
<span *ngIf="[Link]('password')?.errors?.
['required']">Password is required</span>
<span *ngIf="[Link]('password')?.errors?.
['minlength']">Password must be at least 6 characters</span>
</div>
</div>

<div class="error-message" *ngIf="errorMessage">


{{ errorMessage }}
</div>

<button type="submit" [disabled]="[Link] ||


loading">
{{ loading ? 'Logging in...' : 'Login' }}
</button>

<div class="register-link">
Don't have an account? <a routerLink="/
register">Register</a>
</div>
</form>
</div>

/* src/app/auth/login/[Link] */
.login-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background-color: #fff;

h2 {
text-align: center;
margin-bottom: 1.5rem;
color: #333;
}

.form-group {
margin-bottom: 1.5rem;

label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}

input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;

&:focus {
outline: none;
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
}

.error {
color: #d93025;
font-size: 0.875rem;
margin-top: 0.25rem;
}
}

.error-message {
color: #d93025;
margin-bottom: 1rem;
text-align: center;
}

button {
width: 100%;
padding: 0.75rem;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;

&:hover {
background-color: #3367d6;
}

&:disabled {
background-color: #a8c7fa;
cursor: not-allowed;
}
}

.register-link {
text-align: center;
margin-top: 1rem;

a {
color: #4285f4;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}
}
}
Register Component

// src/app/auth/register/[Link]
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule,
Validators } from '@angular/forms';
import { Router, RouterModule } from '@angular/router';
import { AuthService } from '../[Link]';

@Component({
selector: 'app-register',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterModule],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class RegisterComponent {
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private router = inject(Router);

registerForm: FormGroup = [Link]({


displayName: ['', [[Link]]],
email: ['', [[Link], [Link]]],
password: ['', [[Link],
[Link](6)]],
confirmPassword: ['', [[Link]]]
}, { validators: [Link] });

errorMessage: string = '';


loading: boolean = false;

passwordMatchValidator(form: FormGroup) {
const password = [Link]('password')?.value;
const confirmPassword = [Link]('confirmPassword')?.value;

return password === confirmPassword ? null : {


passwordMismatch: true };
}

onSubmit(): void {
if ([Link]) {
return;
}

[Link] = true;
[Link] = '';

const { displayName, email, password } =


[Link];
[Link](email, password,
displayName).subscribe({
next: () => {
[Link] = false;
[Link](['/todos']);
},
error: (error) => {
[Link] = false;
[Link] = [Link];
}
});
}
}

<!-- src/app/auth/register/[Link] -->


<div class="register-container">
<h2>Register</h2>

<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">


<div class="form-group">
<label for="displayName">Name</label>
<input
type="text"
id="displayName"
formControlName="displayName"
placeholder="Enter your name"
>
<div class="error"
*ngIf="[Link]('displayName')?.invalid &&
[Link]('displayName')?.touched">
<span *ngIf="[Link]('displayName')?.errors?.
['required']">Name is required</span>
</div>
</div>

<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
formControlName="email"
placeholder="Enter your email"
>
<div class="error"
*ngIf="[Link]('email')?.invalid &&
[Link]('email')?.touched">
<span *ngIf="[Link]('email')?.errors?.
['required']">Email is required</span>
<span *ngIf="[Link]('email')?.errors?.
['email']">Please enter a valid email</span>
</div>
</div>

<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
formControlName="password"
placeholder="Enter your password"
>
<div class="error"
*ngIf="[Link]('password')?.invalid &&
[Link]('password')?.touched">
<span *ngIf="[Link]('password')?.errors?.
['required']">Password is required</span>
<span *ngIf="[Link]('password')?.errors?.
['minlength']">Password must be at least 6 characters</span>
</div>
</div>

<div class="form-group">
<label for="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
formControlName="confirmPassword"
placeholder="Confirm your password"
>
<div class="error"
*ngIf="[Link]('confirmPassword')?.invalid &&
[Link]('confirmPassword')?.touched">
<span
*ngIf="[Link]('confirmPassword')?.errors?.
['required']">Confirm Password is required</span>
</div>
<div class="error" *ngIf="[Link]?.
['passwordMismatch'] &&
[Link]('confirmPassword')?.touched">
Passwords do not match
</div>
</div>

<div class="error-message" *ngIf="errorMessage">


{{ errorMessage }}
</div>

<button type="submit" [disabled]="[Link] ||


loading">
{{ loading ? 'Registering...' : 'Register' }}
</button>
<div class="login-link">
Already have an account? <a routerLink="/login">Login</a>
</div>
</form>
</div>

/* src/app/auth/register/[Link] */
.register-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background-color: #fff;

h2 {
text-align: center;
margin-bottom: 1.5rem;
color: #333;
}

.form-group {
margin-bottom: 1.5rem;

label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}

input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;

&:focus {
outline: none;
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
}

.error {
color: #d93025;
font-size: 0.875rem;
margin-top: 0.25rem;
}
}

.error-message {
color: #d93025;
margin-bottom: 1rem;
text-align: center;
}

button {
width: 100%;
padding: 0.75rem;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;

&:hover {
background-color: #3367d6;
}

&:disabled {
background-color: #a8c7fa;
cursor: not-allowed;
}
}

.login-link {
text-align: center;
margin-top: 1rem;

a {
color: #4285f4;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}
}
}

Implementing Route Guards


To protect routes that require authentication, let's create an auth guard:
# Create auth guard
touch src/app/auth/[Link]

// src/app/auth/[Link]
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { map, take } from 'rxjs/operators';
import { AuthService } from './[Link]';

export const authGuard: CanActivateFn = (route, state) => {


const router = inject(Router);
const authService = inject(AuthService);

return [Link]$.pipe(
take(1),
map(isLoggedIn => {
if (isLoggedIn) {
return true;
} else {
[Link](['/login'], { queryParams: { returnUrl:
[Link] } });
return false;
}
})
);
};

This guard: - Uses the functional guard approach introduced in Angular 14+ - Checks if
the user is logged in - Redirects to the login page if not authenticated - Preserves the
intended destination URL as a query parameter

Updating Routes
Now, let's update the application routes to include our authentication components and
guards:

// src/app/[Link]
import { Routes } from '@angular/router';
import { LoginComponent } from './auth/login/[Link]';
import { RegisterComponent } from './auth/register/
[Link]';
import { authGuard } from './auth/[Link]';

export const routes: Routes = [


{ path: '', redirectTo: '/todos', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{
path: 'todos',
loadComponent: () => import('./todos/todo-list/todo-
[Link]').then(c => [Link]),
canActivate: [authGuard]
},
{ path: '**', redirectTo: '/todos' }
];

This routing configuration: - Redirects the root path to the todos page - Provides routes
for login and register - Lazy loads the todo list component - Protects the todos route with
our auth guard - Handles unknown routes with a redirect

Creating a Navigation Component


Let's create a navigation component that shows different options based on
authentication status:

# Create navigation component


mkdir -p src/app/shared/navigation
touch src/app/shared/navigation/[Link]
touch src/app/shared/navigation/[Link]
touch src/app/shared/navigation/[Link]

// src/app/shared/navigation/[Link]
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { AuthService } from '../../auth/[Link]';

@Component({
selector: 'app-navigation',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class NavigationComponent {
private authService = inject(AuthService);

user$ = [Link]$;

logout(): void {
[Link]().subscribe();
}
}

<!-- src/app/shared/navigation/[Link] -->


<nav class="navbar">
<div class="navbar-brand">
<a routerLink="/">Firebase Todo App</a>
</div>

<div class="navbar-menu">
<ng-container *ngIf="user$ | async as user; else
notLoggedIn">
<span class="user-greeting">Hello, {{ [Link] ||
'User' }}</span>
<a routerLink="/todos" class="nav-link">My Todos</a>
<button class="logout-button" (click)="logout()">Logout</
button>
</ng-container>

<ng-template #notLoggedIn>
<a routerLink="/login" class="nav-link">Login</a>
<a routerLink="/register" class="nav-link">Register</a>
</ng-template>
</div>
</nav>

/* src/app/shared/navigation/[Link] */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: #4285f4;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

.navbar-brand {
a {
color: white;
font-size: 1.25rem;
font-weight: 600;
text-decoration: none;
}
}

.navbar-menu {
display: flex;
align-items: center;
gap: 1.5rem;
.user-greeting {
margin-right: 0.5rem;
}

.nav-link {
color: white;
text-decoration: none;
font-weight: 500;

&:hover {
text-decoration: underline;
}
}

.logout-button {
background-color: transparent;
border: 1px solid white;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: 500;

&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
}

@media (max-width: 768px) {


.navbar {
flex-direction: column;
padding: 1rem;

.navbar-brand {
margin-bottom: 1rem;
}

.navbar-menu {
width: 100%;
justify-content: center;
}
}
}

Updating App Component


Finally, let's update the app component to include our navigation:
// src/app/[Link]
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { NavigationComponent } from './shared/navigation/
[Link]';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NavigationComponent],
template: `
<app-navigation></app-navigation>
<main class="container">
<router-outlet></router-outlet>
</main>
`,
styles: [`
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
`]
})
export class AppComponent {
title = 'angular-firebase-todo';
}

With these components and services in place, we have implemented a complete


authentication system using Firebase Authentication in our Angular 19 application.
Users can register, login, and logout, and protected routes are only accessible to
authenticated users.

In the next section, we'll implement Firestore database integration for our To-Do
application.

6. Firestore Database Integration


In this section, we'll implement Firestore database integration for our To-Do application,
allowing users to create, read, update, and delete to-do items.

Firestore Data Model


Before writing code, let's define our Firestore data model for the To-Do application:
// src/app/todos/models/[Link]
export interface Todo {
id?: string;
title: string;
description?: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
dueDate?: Date;
userId: string;
attachmentUrl?: string;
attachmentName?: string;
}

This model includes: - Basic to-do properties (title, description, completion status) -
Timestamps for creation and updates - Due date for task deadlines - User ID to associate
todos with specific users - Optional attachment fields for file storage integration

Creating the Todo Service


Now, let's create a service to handle Firestore operations:

# Create todos directory and service


mkdir -p src/app/todos/services
touch src/app/todos/services/[Link]

// src/app/todos/services/[Link]
import { Injectable, inject } from '@angular/core';
import {
Firestore,
collection,
collectionData,
doc,
addDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
serverTimestamp,
DocumentReference,
Timestamp
} from '@angular/fire/firestore';
import { Observable, from, map, switchMap } from 'rxjs';
import { AuthService } from '../../auth/[Link]';
import { Todo } from '../models/[Link]';
@Injectable({
providedIn: 'root'
})
export class TodoService {
private firestore: Firestore = inject(Firestore);
private authService: AuthService = inject(AuthService);

private todosCollection = collection([Link], 'todos');

// Get all todos for the current user


getTodos(): Observable<Todo[]> {
return [Link]$.pipe(
switchMap(user => {
if (!user) {
return [];
}

const todosQuery = query(


[Link],
where('userId', '==', [Link]),
orderBy('createdAt', 'desc')
);

return collectionData(todosQuery, { idField:


'id' }).pipe(
map(todos => [Link](todos as any[]))
);
})
);
}

// Get completed todos


getCompletedTodos(): Observable<Todo[]> {
return [Link]$.pipe(
switchMap(user => {
if (!user) {
return [];
}

const todosQuery = query(


[Link],
where('userId', '==', [Link]),
where('completed', '==', true),
orderBy('createdAt', 'desc')
);

return collectionData(todosQuery, { idField:


'id' }).pipe(
map(todos => [Link](todos as any[]))
);
})
);
}

// Get incomplete todos


getIncompleteTodos(): Observable<Todo[]> {
return [Link]$.pipe(
switchMap(user => {
if (!user) {
return [];
}

const todosQuery = query(


[Link],
where('userId', '==', [Link]),
where('completed', '==', false),
orderBy('createdAt', 'desc')
);

return collectionData(todosQuery, { idField:


'id' }).pipe(
map(todos => [Link](todos as any[]))
);
})
);
}

// Add a new todo


addTodo(todo: Omit<Todo, 'id' | 'createdAt' | 'updatedAt' |
'userId'>): Observable<DocumentReference> {
const user = [Link]();

if (!user) {
throw new Error('User must be logged in to add a todo');
}

const newTodo = {
...todo,
userId: [Link],
completed: [Link] || false,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
};

return from(addDoc([Link], newTodo));


}

// Update a todo
updateTodo(id: string, changes: Partial<Omit<Todo, 'id' |
'createdAt' | 'userId'>>): Observable<void> {
const todoDoc = doc([Link], `todos/${id}`);
const updatedTodo = {
...changes,
updatedAt: serverTimestamp()
};

return from(updateDoc(todoDoc, updatedTodo));


}

// Delete a todo
deleteTodo(id: string): Observable<void> {
const todoDoc = doc([Link], `todos/${id}`);
return from(deleteDoc(todoDoc));
}

// Toggle todo completion status


toggleTodoCompletion(todo: Todo): Observable<void> {
return [Link]([Link]!, { completed: !
[Link] });
}

// Helper method to convert Firestore Timestamps to JavaScript


Dates
private convertTimestamps(todos: any[]): Todo[] {
return [Link](todo => {
const converted: Todo = {
...todo,
createdAt: [Link] ? ([Link] as
Timestamp).toDate() : new Date(),
updatedAt: [Link] ? ([Link] as
Timestamp).toDate() : new Date()
};

if ([Link]) {
[Link] = ([Link] as
Timestamp).toDate();
}

return converted;
});
}
}

This service provides: - CRUD operations for to-do items - Filtering for completed and
incomplete todos - User-specific data isolation - Automatic timestamp handling -
Conversion between Firestore Timestamps and JavaScript Dates

Creating Todo Components


Now, let's create the components for our To-Do application:
# Create todo list component
mkdir -p src/app/todos/todo-list
touch src/app/todos/todo-list/[Link]
touch src/app/todos/todo-list/[Link]
touch src/app/todos/todo-list/[Link]

# Create todo item component


mkdir -p src/app/todos/todo-item
touch src/app/todos/todo-item/[Link]
touch src/app/todos/todo-item/[Link]
touch src/app/todos/todo-item/[Link]

# Create todo form component


mkdir -p src/app/todos/todo-form
touch src/app/todos/todo-form/[Link]
touch src/app/todos/todo-form/[Link]
touch src/app/todos/todo-form/[Link]

Todo List Component

// src/app/todos/todo-list/[Link]
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TodoService } from '../services/[Link]';
import { TodoItemComponent } from '../todo-item/todo-
[Link]';
import { TodoFormComponent } from '../todo-form/todo-
[Link]';
import { Todo } from '../models/[Link]';
import { Observable } from 'rxjs';

@Component({
selector: 'app-todo-list',
standalone: true,
imports: [CommonModule, TodoItemComponent, TodoFormComponent],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class TodoListComponent implements OnInit {
private todoService = inject(TodoService);

todos$!: Observable<Todo[]>;
activeFilter: 'all' | 'active' | 'completed' = 'all';

ngOnInit(): void {
[Link]('all');
}

filterTodos(filter: 'all' | 'active' | 'completed'): void {


[Link] = filter;

switch (filter) {
case 'all':
[Link]$ = [Link]();
break;
case 'active':
[Link]$ = [Link]();
break;
case 'completed':
[Link]$ = [Link]();
break;
}
}

onTodoAdded(): void {
// Refresh the list when a new todo is added
[Link]([Link]);
}

onTodoToggled(todo: Todo): void {


[Link](todo).subscribe();
}

onTodoDeleted(todoId: string): void {


[Link](todoId).subscribe();
}
}

<!-- src/app/todos/todo-list/[Link] -->


<div class="todo-container">
<h1>My Todo List</h1>

<div class="filters">
<button
[[Link]]="activeFilter === 'all'"
(click)="filterTodos('all')"
>
All
</button>
<button
[[Link]]="activeFilter === 'active'"
(click)="filterTodos('active')"
>
Active
</button>
<button
[[Link]]="activeFilter === 'completed'"
(click)="filterTodos('completed')"
>
Completed
</button>
</div>

<app-todo-form (todoAdded)="onTodoAdded()"></app-todo-form>

<div class="todo-list">
<ng-container *ngIf="todos$ | async as todos">
<div *ngIf="[Link] === 0" class="empty-state">
No todos found. Add a new one above!
</div>

<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
(toggleComplete)="onTodoToggled($event)"
(deleteTodo)="onTodoDeleted($event)"
></app-todo-item>
</ng-container>
</div>
</div>

/* src/app/todos/todo-list/[Link] */
.todo-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;

h1 {
text-align: center;
margin-bottom: 2rem;
color: #333;
}

.filters {
display: flex;
justify-content: center;
margin-bottom: 2rem;
gap: 0.5rem;

button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background-color: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;

&:hover {
background-color: #f5f5f5;
}

&.active {
background-color: #4285f4;
color: white;
border-color: #4285f4;
}
}
}

.todo-list {
margin-top: 2rem;

.empty-state {
text-align: center;
padding: 2rem;
background-color: #f9f9f9;
border-radius: 8px;
color: #666;
}
}
}

@media (max-width: 768px) {


.todo-container {
padding: 1rem;

.filters {
flex-wrap: wrap;

button {
flex: 1;
min-width: 80px;
}
}
}
}

Todo Item Component

// src/app/todos/todo-item/[Link]
import { Component, EventEmitter, Input, Output } from
'@angular/core';
import { CommonModule } from '@angular/common';
import { Todo } from '../models/[Link]';

@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class TodoItemComponent {
@Input() todo!: Todo;
@Output() toggleComplete = new EventEmitter<Todo>();
@Output() deleteTodo = new EventEmitter<string>();

onToggleComplete(): void {
[Link]([Link]);
}

onDelete(): void {
[Link]([Link]);
}

formatDate(date: Date | undefined): string {


if (!date) return '';
return new Date(date).toLocaleDateString();
}
}

<!-- src/app/todos/todo-item/[Link] -->


<div class="todo-item" [[Link]]="[Link]">
<div class="todo-checkbox">
<input
type="checkbox"
[checked]="[Link]"
(change)="onToggleComplete()"
id="todo-{{[Link]}}"
>
<label for="todo-{{[Link]}}"></label>
</div>

<div class="todo-content">
<h3 class="todo-title">{{ [Link] }}</h3>

<p *ngIf="[Link]" class="todo-description">


{{ [Link] }}
</p>

<div class="todo-meta">
<span *ngIf="[Link]" class="todo-due-date">
Due: {{ formatDate([Link]) }}
</span>

<span *ngIf="[Link]" class="todo-attachment">


<a [href]="[Link]" target="_blank">
{{ [Link] || 'Attachment' }}
</a>
</span>
</div>
</div>

<button class="delete-button" (click)="onDelete()">


<span class="delete-icon">×</span>
</button>
</div>

/* src/app/todos/todo-item/[Link] */
.todo-item {
display: flex;
align-items: flex-start;
padding: 1rem;
margin-bottom: 1rem;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.2s;

&.completed {
opacity: 0.7;

.todo-title {
text-decoration: line-through;
color: #888;
}
}

.todo-checkbox {
margin-right: 1rem;
margin-top: 0.25rem;

input[type="checkbox"] {
display: none;

& + label {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #4285f4;
border-radius: 4px;
cursor: pointer;
position: relative;

&:after {
content: '';
position: absolute;
display: none;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}

&:checked + label {
background-color: #4285f4;

&:after {
display: block;
}
}
}
}

.todo-content {
flex: 1;

.todo-title {
margin: 0 0 0.5rem;
font-size: 1.1rem;
color: #333;
}

.todo-description {
margin: 0 0 0.75rem;
color: #666;
font-size: 0.9rem;
}

.todo-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.8rem;
color: #888;

.todo-due-date {
display: flex;
align-items: center;

&:before {
content: '⏰';
margin-right: 0.25rem;
}
}
.todo-attachment {
display: flex;
align-items: center;

&:before {
content: '📎';
margin-right: 0.25rem;
}

a {
color: #4285f4;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}
}
}
}

.delete-button {
background: none;
border: none;
color: #d93025;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
margin-left: 0.5rem;
line-height: 1;
opacity: 0.5;
transition: opacity 0.2s;

&:hover {
opacity: 1;
}
}
}

@media (max-width: 768px) {


.todo-item {
flex-direction: column;

.todo-checkbox {
margin-bottom: 0.5rem;
}

.delete-button {
align-self: flex-end;
margin-top: 0.5rem;
}
}
}

Todo Form Component

// src/app/todos/todo-form/[Link]
import { Component, EventEmitter, Output, inject } from
'@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule,
Validators } from '@angular/forms';
import { TodoService } from '../services/[Link]';

@Component({
selector: 'app-todo-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class TodoFormComponent {
private fb = inject(FormBuilder);
private todoService = inject(TodoService);

@Output() todoAdded = new EventEmitter<void>();

showFullForm = false;

todoForm: FormGroup = [Link]({


title: ['', [[Link], [Link](3)]],
description: [''],
dueDate: [null]
});

toggleFullForm(): void {
[Link] = ![Link];
}

onSubmit(): void {
if ([Link]) {
return;
}

const formValue = [Link];

// Convert string date to Date object if present


let dueDate = null;
if ([Link]) {
dueDate = new Date([Link]);
}
[Link]({
title: [Link],
description: [Link],
dueDate: dueDate,
completed: false
}).subscribe({
next: () => {
[Link]();
[Link] = false;
[Link]();
},
error: (error) => {
[Link]('Error adding todo:', error);
}
});
}
}

<!-- src/app/todos/todo-form/[Link] -->


<div class="todo-form-container">
<form [formGroup]="todoForm" (ngSubmit)="onSubmit()">
<div class="form-header" (click)="toggleFullForm()">
<input
type="text"
formControlName="title"
placeholder="Add a new todo..."
class="title-input"
(click)="$[Link]()"
>
<button
type="button"
class="expand-button"
[[Link]]="showFullForm"
>
{{ showFullForm ? '−' : '+' }}
</button>
</div>

<div class="form-details" *ngIf="showFullForm">


<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
formControlName="description"
placeholder="Add details..."
rows="3"
></textarea>
</div>
<div class="form-group">
<label for="dueDate">Due Date</label>
<input
type="date"
id="dueDate"
formControlName="dueDate"
>
</div>

<div class="form-actions">
<button
type="button"
class="cancel-button"
(click)="toggleFullForm()"
>
Cancel
</button>
<button
type="submit"
class="submit-button"
[disabled]="[Link]"
>
Add Todo
</button>
</div>
</div>
</form>
</div>

/* src/app/todos/todo-form/[Link] */
.todo-form-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;

.form-header {
display: flex;
align-items: center;
padding: 1rem;
cursor: pointer;

.title-input {
flex: 1;
border: none;
font-size: 1rem;
padding: 0.5rem;
background-color: transparent;

&:focus {
outline: none;
}
}

.expand-button {
background-color: #4285f4;
color: white;
border: none;
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;

&.expanded {
transform: rotate(180deg);
}
}
}

.form-details {
padding: 0 1rem 1rem;
border-top: 1px solid #eee;

.form-group {
margin-bottom: 1rem;

label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: #555;
}

textarea, input[type="date"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.875rem;

&:focus {
outline: none;
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
}
}

.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;

button {
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
}

.cancel-button {
background-color: transparent;
border: 1px solid #ddd;
color: #555;

&:hover {
background-color: #f5f5f5;
}
}

.submit-button {
background-color: #4285f4;
border: none;
color: white;

&:hover {
background-color: #3367d6;
}

&:disabled {
background-color: #a8c7fa;
cursor: not-allowed;
}
}
}
}
}

Setting Up Firestore Security Rules


Firestore security rules are crucial for protecting your data. Let's update the Firestore
security rules:
# Update Firestore security rules
cat > [Link] << 'EOF'
rules_version = '2';
service [Link] {
match /databases/{database}/documents {
// Allow authenticated users to read and write their own
todos
match /todos/{todoId} {
allow create: if [Link] != null &&
[Link] ==
[Link];
allow read, update, delete: if [Link] != null &&
[Link] ==
[Link];
}

// Default deny
match /{document=**} {
allow read, write: if false;
}
}
}
EOF

These security rules ensure: - Only authenticated users can create todos - Users can only
create todos with their own user ID - Users can only read, update, or delete their own
todos - All other access is denied by default

Implementing Offline Capabilities


Firestore supports offline data persistence, which allows your app to work even when
the user is offline. Let's enable this feature:

// src/app/[Link]
import { ApplicationConfig, importProvidersFrom } from
'@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/
animations';

import { initializeApp, provideFirebaseApp } from '@angular/


fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth';
import {
getFirestore,
provideFirestore,
enableMultiTabIndexedDbPersistence
} from '@angular/fire/firestore';
import { getStorage, provideStorage } from '@angular/fire/
storage';
import { getFunctions, provideFunctions } from '@angular/fire/
functions';
import { getAnalytics, provideAnalytics } from '@angular/fire/
analytics';

import { routes } from './[Link]';


import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {


providers: [
provideRouter(routes),
provideAnimations(),
importProvidersFrom(
provideFirebaseApp(() =>
initializeApp([Link])),
provideAuth(() => getAuth()),
provideFirestore(() => {
const firestore = getFirestore();
// Enable offline persistence
enableMultiTabIndexedDbPersistence(firestore)
.catch(err => {
[Link]('Firestore persistence error:', err);
});
return firestore;
}),
provideStorage(() => getStorage()),
provideFunctions(() => getFunctions()),
provideAnalytics(() => getAnalytics())
)
]
};

This configuration enables offline persistence, allowing users to: - View and interact with
their todos even when offline - Create and modify todos while offline - Have changes
automatically synchronized when back online

Deploying Firestore Rules


To deploy your Firestore security rules:

# Deploy Firestore rules


firebase deploy --only firestore:rules
This command uploads your security rules to Firebase, ensuring they're applied to your
Firestore database.

With these components and services in place, we have implemented a complete


Firestore database integration for our To-Do application. Users can create, read, update,
and delete to-do items, with all data securely stored in Firestore and protected by
appropriate security rules.

In the next section, we'll implement Firebase Storage for file attachments.

7. Firebase Storage Integration


In this section, we'll implement Firebase Storage integration to allow users to attach files
to their to-do items.

Setting Up Firebase Storage Rules


Before implementing the code, let's set up the Firebase Storage security rules:

# Update Storage security rules


cat > [Link] << 'EOF'
rules_version = '2';
service [Link] {
match /b/{bucket}/o {
// Allow authenticated users to read and write their own
files
match /todos/{userId}/{fileName} {
allow read, write: if [Link] != null &&
[Link] == userId;
}

// Default deny
match /{allPaths=**} {
allow read, write: if false;
}
}
}
EOF

These security rules ensure: - Files are organized by user ID to prevent unauthorized
access - Users can only read and write their own files - All other access is denied by
default
Creating a Storage Service
Now, let's create a service to handle file uploads and downloads:

# Create storage service


mkdir -p src/app/shared/services
touch src/app/shared/services/[Link]

// src/app/shared/services/[Link]
import { Injectable, inject } from '@angular/core';
import {
Storage,
ref,
uploadBytesResumable,
getDownloadURL,
deleteObject
} from '@angular/fire/storage';
import { Observable, from, switchMap, of } from 'rxjs';
import { AuthService } from '../../auth/[Link]';

export interface UploadTask {


progress: number;
downloadUrl?: string;
fileName: string;
filePath: string;
}

@Injectable({
providedIn: 'root'
})
export class StorageService {
private storage: Storage = inject(Storage);
private authService: AuthService = inject(AuthService);

// Upload a file to Firebase Storage


uploadFile(file: File): Observable<UploadTask> {
const user = [Link]();

if (!user) {
throw new Error('User must be logged in to upload files');
}

// Create a unique file name


const fileName = `${new Date().getTime()}_${[Link]}`;

// Set the file path in the format: todos/{userId}/


{fileName}
const filePath = `todos/${[Link]}/${fileName}`;
// Create a reference to the file location
const fileRef = ref([Link], filePath);

// Create the upload task


const uploadTask = uploadBytesResumable(fileRef, file);

// Create an observable to track the upload progress


return new Observable<UploadTask>(observer => {
[Link](
'state_changed',
// Progress callback
(snapshot) => {
const progress = ([Link] /
[Link]) * 100;
[Link]({
progress,
fileName: [Link],
filePath
});
},
// Error callback
(error) => {
[Link]('Upload error:', error);
[Link](error);
},
// Completion callback
() => {
// Get the download URL

getDownloadURL([Link]).then(downloadUrl => {
[Link]({
progress: 100,
downloadUrl,
fileName: [Link],
filePath
});
[Link]();
}).catch(error => {
[Link]('Error getting download URL:', error);
[Link](error);
});
}
);

// Return an unsubscribe function


return () => {
// We can't cancel the upload once it's started
// But we can clean up resources if needed
};
});
}
// Get the download URL for a file
getDownloadUrl(filePath: string): Observable<string> {
const fileRef = ref([Link], filePath);
return from(getDownloadURL(fileRef));
}

// Delete a file from Firebase Storage


deleteFile(filePath: string): Observable<void> {
if (!filePath) {
return of(undefined);
}

const fileRef = ref([Link], filePath);


return from(deleteObject(fileRef));
}
}

This service provides: - File upload functionality with progress tracking - Download URL
generation for uploaded files - File deletion capability - User-specific file paths for
security

Updating the Todo Model and Service


Let's update our Todo model and service to support file attachments:

// Update src/app/todos/models/[Link]
export interface Todo {
id?: string;
title: string;
description?: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
dueDate?: Date;
userId: string;
attachmentUrl?: string;
attachmentName?: string;
attachmentPath?: string; // Add this field to store the file
path
}

Now, let's update the Todo service to handle attachments:

// Update src/app/todos/services/[Link]
// Add this method to the TodoService class

// Add a todo with an attachment


addTodoWithAttachment(
todo: Omit<Todo, 'id' | 'createdAt' | 'updatedAt' | 'userId'>,
attachmentUrl: string,
attachmentName: string,
attachmentPath: string
): Observable<DocumentReference> {
const user = [Link]();

if (!user) {
throw new Error('User must be logged in to add a todo');
}

const newTodo = {
...todo,
userId: [Link],
completed: [Link] || false,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
attachmentUrl,
attachmentName,
attachmentPath
};

return from(addDoc([Link], newTodo));


}

// Update a todo's attachment


updateTodoAttachment(
id: string,
attachmentUrl: string,
attachmentName: string,
attachmentPath: string
): Observable<void> {
const todoDoc = doc([Link], `todos/${id}`);
const updates = {
attachmentUrl,
attachmentName,
attachmentPath,
updatedAt: serverTimestamp()
};

return from(updateDoc(todoDoc, updates));


}

// Delete a todo with its attachment


deleteTodoWithAttachment(todo: Todo): Observable<void> {
if (![Link]) {
return throwError(() => new Error('Todo ID is required'));
}

// If the todo has an attachment, delete it first


if ([Link]) {
return
[Link]([Link]).pipe(
switchMap(() => [Link]([Link]!)),
catchError(error => {
[Link]('Error deleting attachment:', error);
// Continue with todo deletion even if attachment
deletion fails
return [Link]([Link]!);
})
);
}

// If no attachment, just delete the todo


return [Link]([Link]);
}

Don't forget to inject the StorageService in the TodoService constructor:

// Add to the TodoService class


private storageService: StorageService = inject(StorageService);

Updating the Todo Form Component


Now, let's update the Todo Form component to support file attachments:

// Update src/app/todos/todo-form/[Link]
import { Component, EventEmitter, Output, inject } from
'@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule,
Validators } from '@angular/forms';
import { TodoService } from '../services/[Link]';
import { StorageService, UploadTask } from '../../shared/
services/[Link]';
import { switchMap } from 'rxjs';

@Component({
selector: 'app-todo-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class TodoFormComponent {
private fb = inject(FormBuilder);
private todoService = inject(TodoService);
private storageService = inject(StorageService);
@Output() todoAdded = new EventEmitter<void>();

showFullForm = false;

todoForm: FormGroup = [Link]({


title: ['', [[Link], [Link](3)]],
description: [''],
dueDate: [null]
});

selectedFile: File | null = null;


uploadProgress: number = 0;
isUploading: boolean = false;

toggleFullForm(): void {
[Link] = ![Link];
}

onFileSelected(event: Event): void {


const input = [Link] as HTMLInputElement;
if ([Link] && [Link] > 0) {
[Link] = [Link][0];
}
}

onSubmit(): void {
if ([Link]) {
return;
}

const formValue = [Link];

// Convert string date to Date object if present


let dueDate = null;
if ([Link]) {
dueDate = new Date([Link]);
}

const todo = {
title: [Link],
description: [Link],
dueDate: dueDate,
completed: false
};

// If there's a file to upload


if ([Link]) {
[Link] = true;

[Link]([Link]).subscribe({
next: (task: UploadTask) => {
[Link] = [Link];

// When upload is complete


if ([Link] === 100 && [Link]) {
[Link](
todo,
[Link],
[Link]!.name,
[Link]
).subscribe({
next: () => [Link](),
error: (error) => [Link](error)
});
}
},
error: (error) => [Link](error)
});
} else {
// No file to upload, just add the todo
[Link](todo).subscribe({
next: () => [Link](),
error: (error) => [Link](error)
});
}
}

private handleSuccess(): void {


[Link]();
[Link] = null;
[Link] = 0;
[Link] = false;
[Link] = false;
[Link]();
}

private handleError(error: any): void {


[Link]('Error:', error);
[Link] = false;
// You might want to show an error message to the user here
}
}

<!-- Update src/app/todos/todo-form/[Link] -->


<div class="todo-form-container">
<form [formGroup]="todoForm" (ngSubmit)="onSubmit()">
<div class="form-header" (click)="toggleFullForm()">
<input
type="text"
formControlName="title"
placeholder="Add a new todo..."
class="title-input"
(click)="$[Link]()"
>
<button
type="button"
class="expand-button"
[[Link]]="showFullForm"
>
{{ showFullForm ? '−' : '+' }}
</button>
</div>

<div class="form-details" *ngIf="showFullForm">


<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
formControlName="description"
placeholder="Add details..."
rows="3"
></textarea>
</div>

<div class="form-group">
<label for="dueDate">Due Date</label>
<input
type="date"
id="dueDate"
formControlName="dueDate"
>
</div>

<div class="form-group">
<label for="attachment">Attachment</label>
<div class="file-input-container">
<input
type="file"
id="attachment"
(change)="onFileSelected($event)"
[disabled]="isUploading"
>
<div class="selected-file" *ngIf="selectedFile">
Selected: {{ [Link] }}
</div>
</div>
</div>

<div class="upload-progress" *ngIf="isUploading">


<div class="progress-bar">
<div class="progress-fill" [[Link].
%]="uploadProgress"></div>
</div>
<div class="progress-
text">{{ [Link](0) }}%</div>
</div>

<div class="form-actions">
<button
type="button"
class="cancel-button"
(click)="toggleFullForm()"
[disabled]="isUploading"
>
Cancel
</button>
<button
type="submit"
class="submit-button"
[disabled]="[Link] || isUploading"
>
{{ isUploading ? 'Uploading...' : 'Add Todo' }}
</button>
</div>
</div>
</form>
</div>

/* Update src/app/todos/todo-form/[Link] */
/* Add these styles to the existing SCSS file */

.file-input-container {
margin-top: 0.5rem;

input[type="file"] {
width: 100%;
padding: 0.5rem 0;

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}

.selected-file {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #555;
}
}

.upload-progress {
margin: 1rem 0;
.progress-bar {
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;

.progress-fill {
height: 100%;
background-color: #4285f4;
transition: width 0.3s ease;
}
}

.progress-text {
text-align: right;
font-size: 0.75rem;
color: #555;
margin-top: 0.25rem;
}
}

Updating the Todo Item Component


Let's update the Todo Item component to handle attachments:

// Update src/app/todos/todo-item/[Link]
// Add this method to the TodoItemComponent class

downloadAttachment(): void {
if ([Link]) {
[Link]([Link], '_blank');
}
}

<!-- Update src/app/todos/todo-item/[Link] -->


<!-- Replace the todo-attachment span with this -->
<span *ngIf="[Link]" class="todo-attachment">
<a (click)="downloadAttachment()" class="attachment-link">
{{ [Link] || 'Attachment' }}
</a>
</span>

/* Update src/app/todos/todo-item/[Link] */
/* Update the todo-attachment styles */
.todo-attachment {
display: flex;
align-items: center;

&:before {
content: '📎';
margin-right: 0.25rem;
}

.attachment-link {
color: #4285f4;
text-decoration: none;
cursor: pointer;

&:hover {
text-decoration: underline;
}
}
}

Updating the Todo List Component


Finally, let's update the Todo List component to handle attachment deletion:

// Update src/app/todos/todo-list/[Link]
// Update the onTodoDeleted method

onTodoDeleted(todo: Todo): void {


[Link](todo).subscribe();
}

<!-- Update src/app/todos/todo-list/[Link] -->


<!-- Update the app-todo-item element -->
<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
(toggleComplete)="onTodoToggled($event)"
(deleteTodo)="onTodoDeleted($event)"
></app-todo-item>

Deploying Storage Rules


To deploy your Storage security rules:
# Deploy Storage rules
firebase deploy --only storage:rules

This command uploads your security rules to Firebase, ensuring they're applied to your
Storage bucket.

Testing File Uploads


To test file uploads:

1. Run your application locally


2. Create a new to-do with an attachment
3. Verify the file uploads successfully
4. Check that the attachment appears in the to-do item
5. Verify you can download the attachment
6. Delete the to-do and verify the attachment is also deleted

With these components and services in place, we have implemented Firebase Storage
integration for our To-Do application. Users can now attach files to their to-do items,
with all files securely stored in Firebase Storage and protected by appropriate security
rules.

In the next section, we'll implement Firebase Cloud Functions for background
processing.

8. Firebase Cloud Functions


In this section, we'll implement Firebase Cloud Functions to add serverless backend
capabilities to our To-Do application.

Setting Up the Cloud Functions Environment


First, let's set up the development environment for Cloud Functions:

# Navigate to the functions directory created by Firebase init


cd functions

# Install dependencies
npm install cors express moment nodemailer
# Return to the project root
cd ..

These dependencies provide: - cors : Cross-Origin Resource Sharing middleware -


express : Web framework for [Link] - moment : Date manipulation library -
nodemailer : Email sending library

Creating Cloud Functions


Now, let's create some useful Cloud Functions for our To-Do application:

// functions/src/[Link]
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as express from 'express';
import * as cors from 'cors';
import * as moment from 'moment';
import * as nodemailer from 'nodemailer';

// Initialize Firebase Admin


[Link]();

// Initialize Firestore
const db = [Link]();

// Create Express app


const app = express();
[Link](cors({ origin: true }));

// API endpoint to get todos for a user


[Link]('/todos/:userId', async (req, res) => {
try {
const userId = [Link];

// Verify authentication
const authHeader = [Link];
if (!authHeader || ![Link]('Bearer ')) {
return [Link](403).json({ error: 'Unauthorized' });
}

const idToken = [Link]('Bearer ')[1];


const decodedToken = await
[Link]().verifyIdToken(idToken);

// Check if the authenticated user is requesting their own


todos
if ([Link] !== userId) {
return [Link](403).json({ error: 'Access denied' });
}

// Get todos from Firestore


const todosSnapshot = await [Link]('todos')
.where('userId', '==', userId)
.orderBy('createdAt', 'desc')
.get();

const todos: any[] = [];


[Link](doc => {
[Link]({
id: [Link],
...[Link]()
});
});

return [Link](200).json(todos);
} catch (error) {
[Link]('Error getting todos:', error);
return [Link](500).json({ error: 'Internal server
error' });
}
});

// Export the API as a Cloud Function


export const api = [Link](app);

// Cloud Function to send reminder emails for upcoming todos


export const sendDueReminders =
[Link]('0 8 * * *')
.timeZone('America/New_York') // Adjust to your timezone
.onRun(async (context) => {
try {
// Get todos due in the next 24 hours
const now = [Link]();
const tomorrow = [Link](
moment().add(1, 'day').toDate()
);

const todosSnapshot = await [Link]('todos')


.where('completed', '==', false)
.where('dueDate', '>=', now)
.where('dueDate', '<=', tomorrow)
.get();

// Group todos by user


const userTodos: Record<string, any[]> = {};

[Link](doc => {
const todo = {
id: [Link],
...[Link]()
};

if (!userTodos[[Link]]) {
userTodos[[Link]] = [];
}

userTodos[[Link]].push(todo);
});

// Send email to each user


const emailPromises = [Link](userTodos).map(async
(userId) => {
const user = await [Link]().getUser(userId);
const email = [Link];

if (!email) {
[Link](`No email found for user ${userId}`);
return;
}

// Create email content


const todos = userTodos[userId];
let todoList = '';

[Link](todo => {
const dueDate =
moment([Link]()).format('MMM DD, YYYY');
todoList += `- ${[Link]} (Due: ${dueDate})\n`;
});

const emailContent = `
Hello ${[Link] || 'there'},

You have the following todos due in the next 24 hours:

${todoList}

Log in to your Todo App to manage these items.

Best regards,
Todo App Team
`;

// Configure email transport (replace with your SMTP


settings)
const transporter = [Link]({
service: 'gmail',
auth: {
user: 'your-email@[Link]',
pass: 'your-app-password'
}
});
// Send the email
await [Link]({
from: '"Todo App" <your-email@[Link]>',
to: email,
subject: 'Upcoming Todo Reminders',
text: emailContent
});

[Link](`Reminder email sent to ${email}`);


});

await [Link](emailPromises);

return null;
} catch (error) {
[Link]('Error sending reminders:', error);
return null;
}
});

// Cloud Function to clean up attachments when todos are deleted


export const cleanupAttachments = [Link]
.document('todos/{todoId}')
.onDelete(async (snapshot, context) => {
try {
const todoData = [Link]();

// Check if the todo had an attachment


if ([Link]) {
// Delete the file from Storage
const file =
[Link]().bucket().file([Link]);
await [Link]();

[Link](`Deleted attachment at path: $


{[Link]}`);
}

return null;
} catch (error) {
[Link]('Error cleaning up attachment:', error);
return null;
}
});

// Cloud Function to track todo statistics


export const updateUserStats = [Link]
.document('todos/{todoId}')
.onWrite(async (change, context) => {
try {
const todoData = [Link] ?
[Link]() : null;
const previousTodoData = [Link] ?
[Link]() : null;

// Get the user ID


let userId: string | null = null;

if (todoData) {
userId = [Link];
} else if (previousTodoData) {
userId = [Link];
}

if (!userId) {
[Link]('No user ID found');
return null;
}

// Get all todos for the user


const todosSnapshot = await [Link]('todos')
.where('userId', '==', userId)
.get();

// Calculate statistics
let totalTodos = 0;
let completedTodos = 0;

[Link](doc => {
totalTodos++;
if ([Link]().completed) {
completedTodos++;
}
});

// Update user statistics in Firestore


const statsRef = [Link]('userStats').doc(userId);
await [Link]({
totalTodos,
completedTodos,
completionRate: totalTodos > 0 ? (completedTodos /
totalTodos) * 100 : 0,
lastUpdated:
[Link]()
}, { merge: true });

[Link](`Updated stats for user ${userId}`);


return null;
} catch (error) {
[Link]('Error updating user stats:', error);
return null;
}
});
These Cloud Functions provide: - A secure REST API endpoint to get todos - Scheduled
reminders for upcoming todos - Automatic cleanup of attachments when todos are
deleted - User statistics tracking

Creating a Cloud Functions Service


Now, let's create a service in our Angular application to interact with our Cloud
Functions:

# Create cloud functions service


mkdir -p src/app/shared/services
touch src/app/shared/services/[Link]

// src/app/shared/services/[Link]
import { Injectable, inject } from '@angular/core';
import { Functions, httpsCallable } from '@angular/fire/
functions';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, from, switchMap } from 'rxjs';
import { AuthService } from '../../auth/[Link]';
import { Todo } from '../../todos/models/[Link]';

export interface UserStats {


totalTodos: number;
completedTodos: number;
completionRate: number;
lastUpdated: Date;
}

@Injectable({
providedIn: 'root'
})
export class FunctionsService {
private functions: Functions = inject(Functions);
private http: HttpClient = inject(HttpClient);
private authService: AuthService = inject(AuthService);

// Base URL for your Cloud Functions API


// Replace with your actual Firebase project ID
private apiBaseUrl = '[Link]
[Link]/api';

// Get todos using the REST API Cloud Function


getTodosViaApi(): Observable<Todo[]> {
return [Link]$.pipe(
switchMap(user => {
if (!user) {
return [];
}

return from([Link]()).pipe(
switchMap(token => {
const headers = new HttpHeaders({
'Authorization': `Bearer ${token}`
});

return [Link]<Todo[]>(
`${[Link]}/todos/${[Link]}`,
{ headers }
);
})
);
})
);
}

// Get user statistics


getUserStats(): Observable<UserStats> {
return [Link]$.pipe(
switchMap(user => {
if (!user) {
throw new Error('User must be logged in');
}

const getUserStats = httpsCallable<{ userId: string },


UserStats>(
[Link],
'getUserStats'
);

return from(getUserStats({ userId: [Link] })).pipe(


switchMap(result => {
return [[Link]];
})
);
})
);
}
}

Note: We need to add a new Cloud Function to get user statistics:

// Add this to functions/src/[Link]


export const getUserStats = [Link](async (data,
context) => {
// Ensure the user is authenticated
if (![Link]) {
throw new [Link](
'unauthenticated',
'The function must be called while authenticated.'
);
}

const userId = [Link];

// Ensure the user is requesting their own stats


if ([Link] !== userId) {
throw new [Link](
'permission-denied',
'Users can only access their own statistics.'
);
}

try {
// Get user statistics from Firestore
const statsDoc = await
[Link]('userStats').doc(userId).get();

if (![Link]) {
// Return default stats if none exist
return {
totalTodos: 0,
completedTodos: 0,
completionRate: 0,
lastUpdated: null
};
}

return [Link]();
} catch (error) {
[Link]('Error getting user stats:', error);
throw new [Link](
'internal',
'An error occurred while retrieving user statistics.'
);
}
});

Creating a User Stats Component


Let's create a component to display user statistics:

# Create user stats component


mkdir -p src/app/shared/user-stats
touch src/app/shared/user-stats/[Link]
touch src/app/shared/user-stats/[Link]
touch src/app/shared/user-stats/[Link]

// src/app/shared/user-stats/[Link]
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FunctionsService, UserStats } from '../services/
[Link]';
import { Observable } from 'rxjs';

@Component({
selector: 'app-user-stats',
standalone: true,
imports: [CommonModule],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
export class UserStatsComponent implements OnInit {
private functionsService = inject(FunctionsService);

stats$!: Observable<UserStats>;

ngOnInit(): void {
[Link]$ = [Link]();
}

formatDate(date: Date | null): string {


if (!date) return 'Never';
return new Date(date).toLocaleString();
}
}

<!-- src/app/shared/user-stats/[Link] -->


<div class="stats-container">
<h3>Your Todo Statistics</h3>

<ng-container *ngIf="stats$ | async as stats; else loading">


<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ [Link] }}</div>
<div class="stat-label">Total Todos</div>
</div>

<div class="stat-item">
<div class="stat-value">{{ [Link] }}</div>
<div class="stat-label">Completed</div>
</div>

<div class="stat-item">
<div class="stat-
value">{{ [Link](0) }}%</div>
<div class="stat-label">Completion Rate</div>
</div>
</div>

<div class="stats-footer">
Last updated: {{ formatDate([Link]) }}
</div>
</ng-container>

<ng-template #loading>
<div class="loading-stats">
Loading statistics...
</div>
</ng-template>
</div>

/* src/app/shared/user-stats/[Link] */
.stats-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
padding: 1.5rem;
margin-bottom: 2rem;

h3 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
font-size: 1.25rem;
text-align: center;
}

.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;

.stat-item {
text-align: center;

.stat-value {
font-size: 2rem;
font-weight: 600;
color: #4285f4;
}

.stat-label {
font-size: 0.875rem;
color: #666;
margin-top: 0.5rem;
}
}
}

.stats-footer {
margin-top: 1.5rem;
text-align: center;
font-size: 0.75rem;
color: #888;
}

.loading-stats {
text-align: center;
padding: 2rem 0;
color: #666;
}
}

@media (max-width: 768px) {


.stats-grid {
grid-template-columns: 1fr !important;
gap: 1.5rem !important;
}
}

Updating the Todo List Component


Let's update the Todo List component to include the user stats:

// Update src/app/todos/todo-list/[Link]
// Add UserStatsComponent to imports
import { UserStatsComponent } from '../../shared/user-stats/
[Link]';

@Component({
selector: 'app-todo-list',
standalone: true,
imports: [CommonModule, TodoItemComponent, TodoFormComponent,
UserStatsComponent],
templateUrl: './[Link]',
styleUrls: ['./[Link]']
})
<!-- Update src/app/todos/todo-list/[Link] -->
<!-- Add this before the todo-list div -->
<app-user-stats></app-user-stats>

Deploying Cloud Functions


To deploy your Cloud Functions:

# Deploy Cloud Functions


firebase deploy --only functions

This command uploads your Cloud Functions to Firebase, making them available for
use.

Security Considerations for Cloud Functions


When implementing Cloud Functions, consider these security best practices:

1. Authentication: Always verify the user's identity before allowing access to


sensitive data.

2. Authorization: Ensure users can only access their own data.

3. Input Validation: Validate all input parameters to prevent injection attacks.

4. Error Handling: Avoid exposing sensitive information in error messages.

5. Rate Limiting: Implement rate limiting to prevent abuse.

6. Secrets Management: Store sensitive information like API keys and passwords in
environment variables:

# Set environment variables for Cloud Functions


firebase functions:config:set [Link]="your-
email@[Link]" [Link]="your-app-password"

Then access them in your code:

const email = [Link]().[Link];


const password = [Link]().[Link];
With these components and services in place, we have implemented Firebase Cloud
Functions for our To-Do application. These functions provide serverless backend
capabilities, including a secure API, scheduled reminders, attachment cleanup, and user
statistics tracking.

In the next section, we'll implement deployment with GitLab CI/CD.

9. Deployment with GitLab CI/CD


In this section, we'll set up a GitLab CI/CD pipeline to automate the deployment of our
Angular 19 + Firebase application.

Setting Up GitLab Repository


First, let's create a GitLab repository for our project:

# Initialize Git repository if not already done


git init

# Add .gitignore file


cat > .gitignore << 'EOF'
# See [Link] for more about
ignoring files.

# Compiled output
/dist
/tmp
/out-tsc
/bazel-out

# Node
/node_modules
[Link]
[Link]

# IDEs and editors


.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# Visual Studio Code


.vscode/*
!.vscode/[Link]
!.vscode/[Link]
!.vscode/[Link]
!.vscode/[Link]
.history/*

# Miscellaneous
/.angular/cache
.sass-cache/
/[Link]
/coverage
/[Link]
[Link]
/typings

# System files
.DS_Store
[Link]

# Firebase
.firebase/
[Link]
[Link]
.[Link]

# Environment files
.env
/src/environments/*.ts
EOF

# Create a placeholder for environment files


mkdir -p src/environments
cat > src/environments/[Link] << 'EOF'
export const environment = {
production: false,
firebase: {
apiKey: "YOUR_API_KEY",
authDomain: "[Link]",
projectId: "your-project-id",
storageBucket: "[Link]",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID"
}
};
EOF

# Add all files to Git


git add .

# Commit changes
git commit -m "Initial commit"
# Add GitLab remote (replace with your GitLab repository URL)
git remote add origin [Link]
[Link]

# Push to GitLab
git push -u origin master

Creating GitLab CI/CD Configuration


Now, let's create a GitLab CI/CD configuration file:

# Create .[Link] file


cat > .[Link] << 'EOF'
image: node:20-alpine

stages:
- setup
- test
- build
- deploy

# Cache node_modules for faster builds


cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/

# Install dependencies
setup:
stage: setup
script:
- npm ci --cache .npm --prefer-offline
artifacts:
paths:
- node_modules/

# Run tests
test:
stage: test
script:
- npm run test:ci
dependencies:
- setup

# Build the application


build:
stage: build
script:
# Create [Link] file from CI/CD variables
- |
cat > src/environments/[Link] << EOL
export const environment = {
production: true,
firebase: {
apiKey: "${FIREBASE_API_KEY}",
authDomain: "${FIREBASE_AUTH_DOMAIN}",
projectId: "${FIREBASE_PROJECT_ID}",
storageBucket: "${FIREBASE_STORAGE_BUCKET}",
messagingSenderId: "${FIREBASE_MESSAGING_SENDER_ID}",
appId: "${FIREBASE_APP_ID}",
measurementId: "${FIREBASE_MEASUREMENT_ID}"
}
};
EOL

# Create [Link] file from CI/CD


variables
- |
cat > src/environments/[Link] << EOL
export const environment = {
production: false,
firebase: {
apiKey: "${FIREBASE_API_KEY}",
authDomain: "${FIREBASE_AUTH_DOMAIN}",
projectId: "${FIREBASE_PROJECT_ID}",
storageBucket: "${FIREBASE_STORAGE_BUCKET}",
messagingSenderId: "${FIREBASE_MESSAGING_SENDER_ID}",
appId: "${FIREBASE_APP_ID}",
measurementId: "${FIREBASE_MEASUREMENT_ID}"
}
};
EOL

# Build the application


- npm run build
dependencies:
- setup
artifacts:
paths:
- dist/

# Deploy to Firebase
deploy:
stage: deploy
image: node:20-alpine
script:
# Install Firebase CLI
- npm install -g firebase-tools
# Create .firebaserc file
- |
cat > .firebaserc << EOL
{
"projects": {
"default": "${FIREBASE_PROJECT_ID}"
}
}
EOL

# Create firebase token file


- echo "${FIREBASE_TOKEN}" > ./firebase-token

# Deploy to Firebase
- firebase deploy --only hosting --token "$(cat ./firebase-
token)" --non-interactive
dependencies:
- build
only:
- master # Only deploy from the master branch
EOF

# Add test:ci script to [Link]


# This is a placeholder - you'll need to manually update your
[Link]
echo "Add the following to your [Link] scripts section:"
echo '"test:ci": "ng test --watch=false --
browsers=ChromeHeadless"'

This CI/CD configuration: - Uses a [Link] Alpine image for faster builds - Caches
node_modules for improved performance - Sets up a pipeline with setup, test, build, and
deploy stages - Creates environment files from GitLab CI/CD variables - Builds the
Angular application - Deploys to Firebase Hosting

Setting Up GitLab CI/CD Variables


In GitLab, you need to set up the following CI/CD variables:

1. Go to your GitLab project


2. Navigate to Settings > CI/CD
3. Expand the Variables section
4. Add the following variables:

FIREBASE_API_KEY
FIREBASE_AUTH_DOMAIN
FIREBASE_PROJECT_ID
FIREBASE_STORAGE_BUCKET
FIREBASE_MESSAGING_SENDER_ID
FIREBASE_APP_ID
FIREBASE_MEASUREMENT_ID
FIREBASE_TOKEN

For the FIREBASE_TOKEN , you need to generate a CI token:

# Login to Firebase
firebase login:ci

This command will open a browser window for authentication and then display a token.
Copy this token and add it as the FIREBASE_TOKEN variable in GitLab.

Configuring Firebase for CI/CD Deployment


To prepare Firebase for CI/CD deployment, we need to update the [Link] file:

# Update [Link]
cat > [Link] << 'EOF'
{
"firestore": {
"rules": "[Link]",
"indexes": "[Link]"
},
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
],
"source": "functions"
},
"hosting": {
"public": "dist/angular-firebase-todo/browser",
"ignore": [
"[Link]",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/[Link]"
}
]
},
"storage": {
"rules": "[Link]"
},
"emulators": {
"auth": {
"port": 9099
},
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"hosting": {
"port": 5000
},
"storage": {
"port": 9199
},
"ui": {
"enabled": true
}
}
}
EOF

Make sure the public path matches your Angular output directory. For Angular 19, it's
typically dist/angular-firebase-todo/browser .

Testing the CI/CD Pipeline


To test your CI/CD pipeline:

1. Commit and push your changes to GitLab:

git add .[Link] [Link]


git commit -m "Add GitLab CI/CD configuration"
git push

1. Go to your GitLab project and navigate to CI/CD > Pipelines to monitor the pipeline
execution.

2. Once the pipeline completes successfully, your application will be deployed to


Firebase Hosting.
Manual Deployment
For manual deployment (outside of CI/CD), you can use the following commands:

# Build the application


ng build

# Deploy to Firebase
firebase deploy

Deploying Specific Firebase Services


You can also deploy specific Firebase services:

# Deploy only Hosting


firebase deploy --only hosting

# Deploy only Firestore rules


firebase deploy --only firestore:rules

# Deploy only Storage rules


firebase deploy --only storage:rules

# Deploy only Functions


firebase deploy --only functions

Rollback Deployments
If you need to rollback to a previous deployment:

# List recent deployments


firebase hosting:versions:list

# Rollback to a specific version


firebase hosting:clone <SOURCE_SITE_ID>:<VERSION>
<DESTINATION_SITE_ID>:live

Replace <SOURCE_SITE_ID> with your Firebase project ID, <VERSION> with the
version to rollback to, and <DESTINATION_SITE_ID> with your Firebase project ID
again.
Monitoring Deployments
You can monitor your deployments in the Firebase console:

1. Go to Firebase Console
2. Select your project
3. Navigate to Hosting
4. View deployment history and status

With this GitLab CI/CD configuration, you have automated the deployment process for
your Angular 19 + Firebase application. The pipeline will build and deploy your
application whenever changes are pushed to the master branch, ensuring a smooth and
consistent deployment process.

In the next section, we'll cover security best practices for your Angular 19 + Firebase
application.

10. Security Best Practices


In this section, we'll cover security best practices for your Angular 19 + Firebase
application, ensuring your data and users are protected.

Firebase Authentication Best Practices

1. Implement Proper Authentication Flow

// src/app/auth/[Link]
// Add these methods to your AuthService

// Password reset functionality


requestPasswordReset(email: string): Observable<void> {
return from(sendPasswordResetEmail([Link], email)).pipe(
catchError(error => {
[Link]('Password reset error:', error);
return throwError(() => new Error(`Password reset failed:
${[Link]}`));
})
);
}

// Email verification
sendEmailVerification(user: User): Observable<void> {
return from(sendEmailVerification(user)).pipe(
catchError(error => {
[Link]('Email verification error:', error);
return throwError(() => new Error(`Email verification
failed: ${[Link]}`));
})
);
}

// Account deletion
deleteAccount(): Observable<void> {
const user = [Link];
if (!user) {
return throwError(() => new Error('No user is currently
logged in'));
}

return from(deleteUser(user)).pipe(
catchError(error => {
[Link]('Account deletion error:', error);
return throwError(() => new Error(`Account deletion
failed: ${[Link]}`));
})
);
}

These methods provide: - Password reset functionality for users who forget their
passwords - Email verification to confirm user identities - Account deletion for users who
want to remove their data

2. Implement Session Management

// src/app/auth/[Link]
// Add this to your AuthService

// Set persistence level


setPersistence(persistenceType: 'local' | 'session' | 'none'):
Observable<void> {
let persistence;

switch (persistenceType) {
case 'local':
persistence = browserLocalPersistence;
break;
case 'session':
persistence = browserSessionPersistence;
break;
case 'none':
persistence = inMemoryPersistence;
break;
default:
persistence = browserLocalPersistence;
}

return from(setPersistence([Link], persistence)).pipe(


catchError(error => {
[Link]('Persistence setting error:', error);
return throwError(() => new Error(`Setting persistence
failed: ${[Link]}`));
})
);
}

Don't forget to import the necessary functions:

import {
// ... existing imports
sendPasswordResetEmail,
sendEmailVerification,
deleteUser,
setPersistence,
browserLocalPersistence,
browserSessionPersistence,
inMemoryPersistence
} from '@angular/fire/auth';

This allows you to control how long the user stays logged in: - local : User remains
logged in even after closing the browser - session : User remains logged in until the
browser is closed - none : User is logged out when the page is refreshed

3. Implement Multi-Factor Authentication

For highly sensitive applications, consider implementing multi-factor authentication:

// src/app/auth/[Link]
// Add these methods to your AuthService

// Start MFA enrollment


startMfaEnrollment(): Observable<MultiFactorSession> {
const user = [Link];
if (!user) {
return throwError(() => new Error('No user is currently
logged in'));
}

const multiFactorUser = multiFactor(user);


return from([Link]()).pipe(
catchError(error => {
[Link]('MFA session error:', error);
return throwError(() => new Error(`MFA session failed: $
{[Link]}`));
})
);
}

// Complete MFA enrollment with phone


completeMfaEnrollment(session: MultiFactorSession, phoneNumber:
string, verificationCode: string): Observable<void> {
const user = [Link];
if (!user) {
return throwError(() => new Error('No user is currently
logged in'));
}

const multiFactorUser = multiFactor(user);


const phoneAuthCredential =
[Link]([Link],
verificationCode);
const multiFactorAssertion =
[Link](phoneAuthCredential);

return from([Link](multiFactorAssertion,
phoneNumber)).pipe(
catchError(error => {
[Link]('MFA enrollment error:', error);
return throwError(() => new Error(`MFA enrollment failed:
${[Link]}`));
})
);
}

Import the necessary MFA functions:

import {
// ... existing imports
multiFactor,
MultiFactorSession,
PhoneAuthProvider,
PhoneMultiFactorGenerator
} from '@angular/fire/auth';
Firestore Security Rules Best Practices

1. Implement Role-Based Access Control

Update your Firestore security rules to implement role-based access control:

// [Link]
rules_version = '2';
service [Link] {
match /databases/{database}/documents {
// Helper function to get user data
function getUserData() {
return get(/databases/$(database)/documents/users/$
([Link])).data
}

// Helper function to check if user is admin


function isAdmin() {
return getUserData().role == 'admin'
}

// Users collection
match /users/{userId} {
// Users can read and update their own data
allow read, update: if [Link] != null &&
[Link] == userId;
// Only admins can create or delete users
allow create, delete: if [Link] != null &&
isAdmin();
}

// Todos collection
match /todos/{todoId} {
// Users can read, create, update, and delete their own
todos
allow read, create, update, delete: if [Link] !=
null &&
[Link] ==
[Link];
// Admins can read all todos
allow read: if [Link] != null && isAdmin();
}

// User stats collection


match /userStats/{userId} {
// Users can read their own stats
allow read: if [Link] != null && [Link]
== userId;
// Only Cloud Functions can write to stats
allow write: if false;
}

// Default deny
match /{document=**} {
allow read, write: if false;
}
}
}

These rules implement: - Role-based access control with admin privileges - User-specific
data isolation - Protection of sensitive collections - Default deny for all other access

2. Validate Data Structure and Content

Add data validation to your Firestore rules:

// [Link]
// Add these validation functions

// Validate todo structure


function isValidTodo() {
let requiredFields = ['title', 'completed', 'userId',
'createdAt', 'updatedAt'];
let optionalFields = ['description', 'dueDate',
'attachmentUrl', 'attachmentName', 'attachmentPath'];
let allFields = [Link](optionalFields);

return [Link]().hasOnly(allFields) &&


[Link](field =>
[Link][field] != null) &&
[Link] is string &&
[Link]() >= 3 &&
[Link]() <= 100 &&
[Link] is bool &&
[Link] is string &&
[Link] == [Link];
}

// Update the todos create rule


match /todos/{todoId} {
allow create: if [Link] != null &&
isValidTodo();
// ... other rules
}

This validation ensures: - Only allowed fields are present - Required fields are not null -
Field types are correct - String lengths are within acceptable ranges - User can only
create todos with their own user ID
Firebase Storage Security Rules Best Practices

1. Implement Size and Type Restrictions

Update your Storage security rules to restrict file sizes and types:

// [Link]
rules_version = '2';
service [Link] {
match /b/{bucket}/o {
// Helper function to check file size
function isValidFileSize() {
return [Link] <= 5 * 1024 * 1024; // 5MB
limit
}

// Helper function to check file type


function isValidFileType() {
return [Link]('image/.*') ||
[Link]('application/
pdf') ||
[Link]('text/plain');
}

// Allow authenticated users to read and write their own


files with restrictions
match /todos/{userId}/{fileName} {
allow read: if [Link] != null && [Link]
== userId;
allow write: if [Link] != null &&
[Link] == userId &&
isValidFileSize() &&
isValidFileType();
}

// Default deny
match /{allPaths=**} {
allow read, write: if false;
}
}
}

These rules implement: - File size restrictions (5MB limit) - File type restrictions (images,
PDFs, and text files only) - User-specific file access
API Key and Secret Management

1. Environment Variables in CI/CD

As we've already set up in the GitLab CI/CD section, use environment variables for
sensitive information:

# .[Link]
build:
script:
# Create [Link] file from CI/CD variables
- |
cat > src/environments/[Link] << EOL
export const environment = {
production: true,
firebase: {
apiKey: "${FIREBASE_API_KEY}",
authDomain: "${FIREBASE_AUTH_DOMAIN}",
// ... other Firebase config
}
};
EOL

2. Runtime Configuration

For even better security, consider using runtime configuration instead of build-time
environment files:

// src/app/config/[Link]
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';

export interface FirebaseConfig {


apiKey: string;
authDomain: string;
projectId: string;
storageBucket: string;
messagingSenderId: string;
appId: string;
measurementId: string;
}

export interface AppConfig {


firebase: FirebaseConfig;
apiUrl: string;
// Other configuration properties
}

@Injectable({
providedIn: 'root'
})
export class AppConfigService {
private config: AppConfig | null = null;

constructor(private http: HttpClient) {}

loadConfig(): Observable<AppConfig> {
if ([Link]) {
return of([Link]);
}

return [Link]<AppConfig>('/assets/[Link]').pipe(
tap(config => {
[Link] = config;
}),
catchError(error => {
[Link]('Could not load configuration', error);
throw error;
})
);
}

getConfig(): AppConfig {
if (![Link]) {
throw new Error('Config not loaded');
}
return [Link];
}
}

Then update your app initialization:

// src/app/[Link]
import { APP_INITIALIZER, ApplicationConfig,
importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/
animations';
import { provideHttpClient } from '@angular/common/http';

import { initializeApp, provideFirebaseApp } from '@angular/


fire/app';
// ... other Firebase imports

import { routes } from './[Link]';


import { AppConfigService } from './config/[Link]';

export function initializeAppFactory(appConfigService:


AppConfigService) {
return () => [Link]().toPromise();
}

export const appConfig: ApplicationConfig = {


providers: [
provideRouter(routes),
provideAnimations(),
provideHttpClient(),
{
provide: APP_INITIALIZER,
useFactory: initializeAppFactory,
deps: [AppConfigService],
multi: true
},
importProvidersFrom(
provideFirebaseApp(() => {
const config = [Link]().firebase;
return initializeApp(config);
}),
// ... other Firebase providers
)
]
};

This approach: - Loads configuration at runtime instead of build time - Allows you to
change configuration without rebuilding the application - Keeps sensitive information
out of your source code

Cross-Site Scripting (XSS) Protection

1. Use Angular's Built-in Sanitization

Angular provides built-in protection against XSS attacks through its template syntax and
sanitization services. However, you should be careful when using certain APIs:

// src/app/shared/[Link]
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-
browser';

@Pipe({
name: 'safeHtml',
standalone: true
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

transform(value: string): SafeHtml {


return [Link](value);
}
}

Use this pipe only when you absolutely need to render HTML from a trusted source:

<!-- Only use with trusted content -->


<div [innerHTML]="trustedHtmlContent | safeHtml"></div>

2. Content Security Policy (CSP)

Add a Content Security Policy to your [Link] :

<!-- src/[Link] -->


<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angular Firebase Todo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-
scale=1">
<link rel="icon" type="image/x-icon" href="[Link]">
<!-- Content Security Policy -->
<meta http-equiv="Content-Security-Policy" content="default-
src 'self'; script-src 'self'; connect-src 'self' https://
*.[Link] [Link] https://
*.[Link]; img-src 'self' data: https://
*.[Link]; style-src 'self' 'unsafe-inline'; font-src
'self';">
</head>
<body>
<app-root></app-root>
</body>
</html>

This CSP: - Restricts script execution to the same origin - Allows connections to Firebase
services - Restricts image sources - Allows inline styles (needed for Angular) - Restricts
font sources
Cross-Site Request Forgery (CSRF) Protection
Firebase Authentication uses tokens for authentication, which provides inherent
protection against CSRF attacks. However, for your own API endpoints, you should
implement CSRF protection:

// functions/src/[Link]
// Add CSRF protection to your API

// Generate a CSRF token


[Link]('/csrf-token', (req, res) => {
const csrfToken = [Link]().toString(36).substring(2);
[Link]('XSRF-TOKEN', csrfToken, {
httpOnly: false,
secure: true,
sameSite: 'strict'
});
[Link](200).json({ csrfToken });
});

// Verify CSRF token


const csrfProtection = (req: any, res: any, next: any) => {
const csrfToken = [Link]['x-xsrf-token'];
const cookieToken = [Link]['XSRF-TOKEN'];

if (!csrfToken || !cookieToken || csrfToken !== cookieToken) {


return [Link](403).json({ error: 'CSRF token validation
failed' });
}

next();
};

// Apply CSRF protection to POST endpoints


[Link]('/todos/:userId', csrfProtection, async (req, res) => {
// Your endpoint logic
});

Rate Limiting and Abuse Prevention


Implement rate limiting in your Cloud Functions to prevent abuse:

// functions/src/[Link]
// Add rate limiting middleware

const rateLimiter = (req: any, res: any, next: any) => {


// Get the user's IP address
const ip = [Link]['x-forwarded-for'] ||
[Link];

// Create a reference to the rate limiting document


const rateLimitRef = [Link]('rateLimits').doc(ip);

// Check if the user has exceeded the rate limit


return [Link](async (transaction) => {
const doc = await [Link](rateLimitRef);

// Current time
const now = [Link]();

// If the document doesn't exist, create it


if (![Link]) {
[Link](rateLimitRef, {
count: 1,
timestamp: now
});
return next();
}

const data = [Link]();

// If the timestamp is more than 1 minute old, reset the


count
if (now - data!.timestamp > 60000) {
[Link](rateLimitRef, {
count: 1,
timestamp: now
});
return next();
}

// If the count is less than 100, increment it


if (data!.count < 100) {
[Link](rateLimitRef, {
count: data!.count + 1
});
return next();
}

// Otherwise, the user has exceeded the rate limit


throw new Error('Rate limit exceeded');
})
.then(() => {
// Transaction succeeded
})
.catch(error => {
if ([Link] === 'Rate limit exceeded') {
return [Link](429).json({ error: 'Too many requests,
please try again later' });
}

[Link]('Rate limiting error:', error);


return [Link](500).json({ error: 'Internal server
error' });
});
};

// Apply rate limiting to all API endpoints


[Link](rateLimiter);

This middleware: - Tracks requests by IP address - Limits users to 100 requests per
minute - Resets the count after 1 minute - Returns a 429 status code when the limit is
exceeded

Regular Security Audits


Regularly audit your application for security vulnerabilities:

1. Dependency Scanning: Use tools like npm audit to check for vulnerabilities in your
dependencies:

# Check for vulnerabilities


npm audit

# Fix vulnerabilities
npm audit fix

1. Code Scanning: Use tools like ESLint with security plugins to scan your code for
security issues:

# Install ESLint security plugin


npm install eslint-plugin-security

# Add to your .[Link]


[Link] = {
plugins: ['security'],
extends: ['plugin:security/recommended']
};

1. Firebase Security Rules Testing: Test your Firestore and Storage security rules:

# Install Firebase rules testing library


npm install -D @firebase/rules-unit-testing
# Create a test file
touch [Link]

// [Link]
const firebase = require('@firebase/rules-unit-testing');
const fs = require('fs');

const projectId = 'firestore-rules-test';

beforeEach(async () => {
await [Link]({ projectId });
});

afterAll(async () => {
await [Link]([Link]().map(app => [Link]()));
});

describe('Firestore security rules', () => {


it('allows users to read their own todos', async () => {
const db = [Link]({
projectId,
auth: { uid: 'user1' }
}).firestore();

// Set up test data


const admin = [Link]({
projectId }).firestore();
await [Link]('todos').doc('todo1').set({
title: 'Test Todo',
completed: false,
userId: 'user1'
});

// Test reading a todo


const todoRef = [Link]('todos').doc('todo1');
await [Link]([Link]());
});

it('prevents users from reading other users\' todos', async


() => {
const db = [Link]({
projectId,
auth: { uid: 'user2' }
}).firestore();

// Set up test data


const admin = [Link]({
projectId }).firestore();
await [Link]('todos').doc('todo1').set({
title: 'Test Todo',
completed: false,
userId: 'user1'
});

// Test reading another user's todo


const todoRef = [Link]('todos').doc('todo1');
await [Link]([Link]());
});

// Add more tests for your security rules


});

By implementing these security best practices, you'll ensure your Angular 19 + Firebase
application is protected against common security threats and vulnerabilities. Remember
to regularly update your dependencies, audit your code, and test your security rules to
maintain a high level of security.

11. Conclusion and Next Steps


In this comprehensive guide, we've built a complete Angular 19 + Firebase To-Do
application with standalone components. We've covered:

1. Setting up an Angular 19 project with standalone components


2. Integrating Firebase services:
3. Authentication for user management
4. Firestore for real-time data storage
5. Storage for file attachments
6. Cloud Functions for serverless backend logic
7. Implementing GitLab CI/CD for automated deployment
8. Applying security best practices to protect your application and data

By following this guide, you've created a robust, production-ready application that


leverages the power of Angular 19's standalone components and Firebase's
comprehensive backend services.

What You've Learned


• How to structure an Angular 19 application using standalone components
• How to implement user authentication with Firebase
• How to create, read, update, and delete data in Firestore
• How to upload and manage files with Firebase Storage
• How to implement serverless functions with Firebase Cloud Functions
• How to automate deployment with GitLab CI/CD
• How to secure your application with proper security rules and best practices

Next Steps
To further enhance your Angular 19 + Firebase application, consider exploring these
advanced topics:

1. Advanced Firebase Features

• Firebase Analytics: Implement analytics to track user behavior and app


performance
• Firebase Remote Config: Add remote configuration to change app behavior
without deploying updates
• Firebase A/B Testing: Test different features with different user segments
• Firebase Performance Monitoring: Monitor app performance in real-time

2. Advanced Angular Features

• Angular Signals: Implement the new signals API for more efficient state
management
• Angular Server-Side Rendering (SSR): Improve SEO and initial load performance
• Angular PWA: Convert your application to a Progressive Web App for offline
capabilities
• Angular Internationalization (i18n): Add multi-language support

3. Testing

• Unit Testing: Implement comprehensive unit tests with Jasmine and Karma
• End-to-End Testing: Add end-to-end tests with Cypress or Playwright
• Firebase Emulator Suite: Use local emulators for testing Firebase services

4. Performance Optimization

• Lazy Loading: Implement lazy loading for all feature modules


• Virtual Scrolling: Use Angular's virtual scrolling for large lists
• Web Workers: Offload heavy computations to web workers
• Service Worker Caching: Implement advanced caching strategies
Resources
Here are some resources to help you continue learning:

• Angular Documentation
• Firebase Documentation
• Angular Fire Documentation
• GitLab CI/CD Documentation

Feedback and Support


If you have any questions or feedback about this guide, please don't hesitate to reach
out. We're always looking to improve and provide the best resources for Angular and
Firebase development.

Happy coding!

You might also like