category: How toMay 15, 2024

How To Build Your Own Newsletter App? p.1

This is a guide on how to build a newsletter application that allows users to subscribe to a mailing list using a Google or GitHub account. It uses Next.js, Firebase, and Novu. Part 1.

TL;DR

In this 2-part tutorial, Iโ€™ll guide you through building a newsletter application that allows users to subscribe to a mailing list using a Google or GitHub account.

The application stores the userโ€™s details (email, first and last names) to Firebase, enabling the admin user to send notifications of beautifully designed email templates to every subscriber within the application usingย Novu.

Youโ€™ll learn how to:

  • implement Firebase Google, GitHub, and Email/Password authentication to your applications
  • Save and fetch data from Firebase Firestore
  • Create and modify email templates in theย Novu Dev Studio
  • Send email templates using the Novu

Building the application interface with Next.js

In this section, Iโ€™ll walk you through building the user interface for the newsletter application.

First, letโ€™s set up a Next.js project. Open your terminal and run the following command:

npx create-next-app newsletter-app

Follow the prompts to select your preferred configuration settings. For this tutorial, weโ€™ll use TypeScript and theย Next.js App Routerย for navigation.

Install theย React Icons package, which allows us to use various icons within the application.

npm install react-icons

The application pages are divided into two parts โ€“ the subscriber routes and admin routes.

  • Subscriber Routes:
    • / โ€“ The application home page that displays GitHub and Google sign-in buttons for user authentication.
    • /subscribe โ€“ This page notifies users that they have successfully subscribed to the newsletter.
  • Admin Routes:
    • /admin โ€“ This route displays the admin sign-up and login buttons.
    • /admin/register โ€“ Here, admins can access the sign-up form to create a new admin account.
    • /admin/login โ€“ Existing admin users can log into the application using their email and password.
    • /admin/dashboard โ€“ Admin users can access this route to view all subscribers and manage the creation and sending of newsletters.

The Subscriber Routes

Copy the code snippet below into app/page.tsx file. It displays the GiHub and Google sign-in buttons.

"use client";
import { useState } from "react";
import { FaGoogle, FaGithub } from "react-icons/fa";

export default function Home() {
	const [loading, setLoading] = useState<boolean>(false);

	const handleGoogleSignIn = () => {
		setLoading(true);
		//๐Ÿ‘‰๐Ÿป Firebase Google Sign in function
	};

	const handleGithubSignIn = () => {
		setLoading(true);
		//๐Ÿ‘‰๐Ÿป Firebase GitHub Sign in function
	};

	return (
		<main className='flex h-screen flex-col p-8 items-center justify-center'>
			<h2 className='text-3xl font-bold mb-6'>Sign up to the Newsletter</h2>
			
			<button
				className='py-3 px-6 flex items-center justify-center rounded-md border-gray-600 border-2 w-3/5 mb-4 text-lg hover:bg-gray-800 hover:text-gray-50'
				disabled={loading}
			>
				<FaGithub className='inline-block mr-2' />
				{loading ? "Signing in..." : "Sign Up with Github"}
			</button>

			<button
				className='py-3 px-6 flex items-center justify-center rounded-md border-gray-600 border-2 w-3/5 mb-4 text-lg hover:bg-gray-800 hover:text-gray-50'
				disabled={loading}
			>
				<FaGoogle className='inline-block mr-2' />
				{loading ? "Signing in..." : "Sign Up with Google"}
			</button>
		</main>
	);
}

Create a subscribe folder within the app directory. Inside the subscribe folder, add a page.tsx file and a layout.tsx file.

cd app
mkdir subscribe && cd subscribe
touch page.tsx layout.tsx

To indicate that the user has successfully subscribed to the newsletter, copy and paste the following code snippet into the subscribe/page.tsx file:

"use client";

export default function Subscribe() {
	const handleSignOut = () => {
		//๐Ÿ‘‰๐Ÿป signout the subscriber
	};

	return (
		<main className='p-8 min-h-screen flex flex-col items-center justify-center'>
			<h3 className='text-3xl font-bold mb-4'>You&apos;ve Subscribed!</h3>
			<button
				className='bg-black text-gray-50 px-8 py-4 rounded-md w-[200px]'
				onClick={handleSignOut}
			>
				Back
			</button>
		</main>
	);
}

Finally, update the subscribe/layout.tsx file with the following changes to set the page title to Subscribe | Newsletter Subscription:

import type { Metadata } from "next";
import { Sora } from "next/font/google";

const sora = Sora({ subsets: ["latin"] });

//๐Ÿ‘‡๐Ÿป changes the page title
export const metadata: Metadata = {
	title: "Subscribe | Newsletter Subscription",
	description: "Generated by create next app",
};

export default function RootLayout({
	children,
}: Readonly<{
	children: React.ReactNode;
}>) {
	return (
		<html lang='en'>
			<body className={sora.className}>{children}</body>
		</html>
	);
}

The Admin Routes

Create an admin folder within the app directory. Inside the admin folder, add a page.tsx file to represent the Admin home page and a layout.tsx file to display the page title.

cd app
mkdir admin && cd admin
touch page.tsx layout.tsx

Copy the code snippet below into the admin/page.tsx file. It displays the links to the Admin Sign-up and Login pages.

import Link from "next/link";

export default function Admin() {
	return (
		<main className='flex min-h-screen flex-col items-center justify-center p-8'>
			<h2 className='text-3xl font-bold mb-6'>Admin Panel</h2>

			<Link
				href='/admin/register'
				className='py-3 px-6 flex items-center justify-center rounded-md border-gray-600 border-2 w-3/5 mb-4 text-lg hover:bg-gray-800 hover:text-gray-50'
			>
				Sign Up as an Admin
			</Link>

			<Link
				href='/admin/login'
				className='py-3 px-6 flex items-center justify-center rounded-md border-gray-600 border-2 w-3/5 mb-4 text-lg hover:bg-gray-800 hover:text-gray-50'
			>
				Log in as an Admin
			</Link>
		</main>
	);
}

Update the page title by copying this code snippet into the admin/layout.tsx file.

import type { Metadata } from "next";
import { Sora } from "next/font/google";

const sora = Sora({ subsets: ["latin"] });

export const metadata: Metadata = {
	title: "Admin | Newsletter Subscription",
	description: "Generated by create next app",
};

export default function RootLayout({
	children,
}: Readonly<{
	children: React.ReactNode;
}>) {
	return (
		<html lang='en'>
			<body className={sora.className}>{children}</body>
		</html>
	);
}

Next, you need to create the Admin Register, Login, and Dashboard pages. Therefore, create the folders containing a page.tsx file.

cd admin
mkdir login register dashboard
cd login && page.tsx
cd ..
cd register && page.tsx
cd ..
cd dashboard && page.tsx

Within the register/page.tsx file, copy and pasted the code snippet provided below. The code block displays the sign-up form where users can enter their email and password to create an admin user account.

"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";

export default function Register() {
	const [email, setEmail] = useState<string>("");
	const [disabled, setDisabled] = useState<boolean>(false);
	const [password, setPassword] = useState<string>("");

	const handleSubmit = (e: React.FormEvent) => {
		e.preventDefault();
		setDisabled(true);
		//๐Ÿ‘‰๐Ÿป admin sign up function
	};
	return (
		<main className='flex min-h-screen flex-col items-center justify-center p-8'>
			<h2 className='font-bold text-3xl text-gray-700 mb-3'>Admin Sign Up</h2>
			<form
				className='md:w-2/3 w-full flex flex-col justify-center'
				onSubmit={handleSubmit}
			>
				<label htmlFor='email' className='text-lg'>
					Email
				</label>
				<input
					type='email'
					id='email'
					value={email}
					onChange={(e) => setEmail(e.target.value)}
					required
					className='w-full py-3 px-6 border-gray-600 border-[1px] rounded-md mb-4'
				/>

				<label htmlFor='password' className='text-lg'>
					Password
				</label>
				<input
					type='password'
					id='password'
					required
					value={password}
					onChange={(e) => setPassword(e.target.value)}
					className='w-full py-3 px-6 border-gray-600 border-[1px] rounded-md mb-4'
				/>

				<button
					className='py-3 px-6 rounded-md bg-black text-gray-50'
					disabled={disabled}
				>
					Register
				</button>
				<p className='text-md mt-4 text-center'>
					Already have an account?{" "}
					<Link href='/admin/login' className='text-blue-500'>
						Log in
					</Link>
				</p>
			</form>
		</main>
	);
}

Update the login/page.tsx file similarly to the admin register page. It accepts the userโ€™s email and password and logs the user into the application.

"use client";
import React, { useState } from "react";
import Link from "next/link";

export default function Login() {
	const [email, setEmail] = useState<string>("");
	const [password, setPassword] = useState<string>("");
	const [disabled, setDisabled] = useState<boolean>(false);

	const handleSubmit = (e: React.FormEvent) => {
		e.preventDefault();
		setDisabled(true);
		//๐Ÿ‘‰๐Ÿป log user into the application
	};
	
	return (
		<main className='flex min-h-screen flex-col items-center justify-center p-8'>
			<h2 className='font-bold text-3xl text-gray-700 mb-3'>Admin Login</h2>
			<form
				className='md:w-2/3 w-full flex flex-col justify-center'
				onSubmit={handleSubmit}
			>
				<label htmlFor='email' className='text-lg'>
					Email
				</label>
				<input
					type='email'
					id='email'
					value={email}
					onChange={(e) => setEmail(e.target.value)}
					placeholder='test@gmail.com'
					required
					className='w-full py-3 px-6 border-gray-600 border-[1px] rounded-md mb-4'
				/>

				<label htmlFor='password' className='text-lg'>
					Password
				</label>
				<input
					type='password'
					id='password'
					required
					placeholder='test123'
					value={password}
					onChange={(e) => setPassword(e.target.value)}
					className='w-full py-3 px-6 border-gray-600 border-[1px] rounded-md mb-4'
				/>

				<button
					className='py-3 px-6 rounded-md bg-black text-gray-50'
					disabled={disabled}
				>
					Log in
				</button>
				<p className='text-md mt-4 text-center'>
					Don&apos;t have an account?{" "}
					<Link href='/admin/register' className='text-blue-500'>
						Create one
					</Link>
				</p>
			</form>
		</main>
	);
}

Finally, letโ€™s create the admin dashboard page. It displays all the available subscribers and a form enabling admin users to create and view the existing newsletters.

Copy the code snippet below into the dashboard/page.tsx file.

"use client";
import { useState } from "react";
import Link from "next/link";
import Newsletters from "@/components/Newsletters";
import SubscribersList from "@/components/SubscribersList";

type Subscriber = {
	id: string;
	data: {
		email: string;
		firstName: string;
		lastName: string;
		topics: string[];
	};
};

export default function Dashboard() {
	const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
	const [toggleView, setToggleView] = useState<boolean>(false);
	const handleToggleView = () => setToggleView(!toggleView);

	const fetchAllSubscribers = async () => {
		//๐Ÿ‘‰๐Ÿป fetch all subscribers
	};

	const handleLogout = () => {
		//๐Ÿ‘‰๐Ÿป sign user out
	};

	return (
		<div className='flex w-full min3-h-screen relative'>
			<div className='lg:w-[15%] border-r-2 lg:flex hidden flex-col justify-between min-h-[100vh]'>
				<nav className='fixed p-4 pb-8 flex flex-col justify-between h-screen'>
					<div className='flex flex-col space-y-4'>
						<h3 className='text-2xl font-bold text-blue-500 mb-6'>Dashboard</h3>
						<Link
							href='#subscribers'
							onClick={handleToggleView}
							className={`${
								toggleView ? "" : "bg-blue-400 text-blue-50"
							}  p-3 mb-2 rounded-md`}
						>
							Subscribers
						</Link>
						<Link
							href='#newsletters'
							onClick={handleToggleView}
							className={`${
								!toggleView ? "" : "bg-blue-400 text-blue-50"
							}  p-3 mb-2 rounded-md`}
						>
							Newsletters
						</Link>
					</div>

					<button className='text-red-500 block mt-10' onClick={handleLogout}>
						Log out
					</button>
				</nav>
			</div>

			<main className='lg:w-[85%] w-full bg-white h-full p-4'>
				<div className='flex items-center lg:justify-end justify-between mb-3'>
					<Link
						href='#'
						onClick={handleToggleView}
						className='lg:hidden block text-blue-700'
					>
						{!toggleView ? "View Newsletters" : "View Subscribers"}
					</Link>
					<button
						className='bg-red-500 text-white px-5 py-3 rounded-md lg:hidden block'
						onClick={handleLogout}
					>
						Log Out
					</button>
				</div>
				<div>
					{toggleView ? (
						<Newsletters />
					) : (
						<SubscribersList subscribers={subscribers} />
					)}
				</div>
			</main>
		</div>
	);
}

From the code snippet above, we have two components: the Newsletters and the SubscribersList.

The Newsletters component renders the existing newsletters and a form that allows the admin to create new ones. The SubscribersList component displays all the existing subscribers within the application.

Therefore, create a components folder that contains the Newsletters and SubscribersList components within the app folder.

cd app
mkdir components && cd components
touch SubscribersList.tsx Newsletters.tsx

Copy the code snippet below into the SubscribersList.tsx. It accepts the existing subscribersโ€™ data as a prop and renders it within a table.

"use client";

type Subscriber = {
	id: string;
	data: {
		email: string;
		firstName: string;
		lastName: string;
		topics: string[];
	};
};

export default function SubscribersList({
	subscribers,
}: {
	subscribers: Subscriber[];
}) {
	return (
		<main className='w-full'>
			<section className='flex items-center justify-between mb-4'>
				<h2 className='font-bold text-2xl mb-3'>All Subscribers</h2>
			</section>
			<div className='w-full'>
				<table className='w-full'>
					<thead>
						<tr>
							<th className='py-3'>First Name</th>
							<th className='py-3'>Last Name</th>
							<th className='py-3'>Email Address</th>
						</tr>
					</thead>
					<tbody>
						{subscribers.map((subscriber) => (
							<tr key={subscriber.id}>
								<td className='py-3'>{subscriber.data.firstName}</td>
								<td className='py-3'>{subscriber.data.lastName}</td>
								<td className='py-3'>{subscriber.data.email}</td>
							</tr>
						))}
					</tbody>
				</table>
			</div>
		</main>
	);
}

Lastly, update the Newsletters.tsx component to display the newsletter creation form and display the existing newsletters.

"use client";
import React, { useEffect, useState } from "react";

export default function Newsletters() {
	const [recipients, setRecipients] = useState<string[]>([]);
	const [subject, setSubject] = useState<string>("");
	const [message, setMessage] = useState<string>("");
	const [disabled, setDisable] = useState<boolean>(false);
	const [subscribers, setSubscribers] = useState<SubscribersData[]>([]);
	const [newsLetters, setNewsletters] = useState<string[]>([]);

	const handleSendNewsletter = (e: React.FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		emailNewsletter();
	};

	const emailNewsletter = async () => {
		//๐Ÿ‘‰๐Ÿป send newsletter
	};

	const fetchRecipients = async () => {
		//๐Ÿ‘‰๐Ÿป fetch all subscribers
	};

	const fetchNewsLetters = async () => {
		//๐Ÿ‘‰๐Ÿป fetch all newsletters
	};

	useEffect(() => {
		fetchRecipients();
		fetchNewsLetters();
	}, []);

	return (
		<main className='w-full'>
			<h2 className='font-bold text-2xl mb-4'>Create Campaign</h2>
			<form onSubmit={handleSendNewsletter} className='mb-8'>
				<label htmlFor='subject'>Subject</label>
				<input
					type='text'
					id='subject'
					value={subject}
					required
					onChange={(e) => setSubject(e.target.value)}
					className='w-full px-4 py-3 border-[1px] border-gray-600 rounded-sm mb-3'
				/>

				<label htmlFor='recipients'>Recipients</label>
				<input
					type='text'
					id='recipients'
					className='w-full px-4 py-3 border-[1px] border-gray-600 rounded-sm mb-3'
					disabled
					readOnly
					value={recipients.join(", ")}
				/>
				<label htmlFor='message'>Message</label>
				<textarea
					id='message'
					rows={5}
					value={message}
					required
					onChange={(e) => setMessage(e.target.value)}
					className='w-full px-4 py-3 border-[1px] border-gray-600 rounded-sm'
				></textarea>
				<button
					className='bg-blue-500 text-white py-3 px-6 rounded my-3'
					disabled={disabled}
				>
					{disabled ? "Sending..." : "Send Newsletter"}
				</button>
			</form>

			<h2 className='font-bold text-2xl '>Recent Newsletters</h2>
			<div className='flex flex-col gap-4'>
				{newsLetters.map((item, index) => (
					<div
						className='flex justify-between items-center bg-gray-100 p-4'
						key={index}
					>
						<h3 className='font-bold text-md'>{item}</h3>
						<button className='bg-green-500 text-gray-50 px-4 py-2 rounded-md'>
							Sent
						</button>
					</div>
				))}
			</div>
		</main>
	);
}

Congratulations!๐Ÿฅณย Youโ€™ve successfully completed the user interface for the application.

In the upcoming section, youโ€™ll learn how to connect the application to a Firebase backend and effectively manipulate data within the application.


How to add Firebase authentication to a Next.js application

In this section, youโ€™ll learn how to implement multiple authentication methods using Firebase within your application.

Subscribers will have the option to subscribe or sign in using either a Google or GitHub account, while Admin users will be able to sign in using the Email and Password authentication method.

Setting up a Firebase project

Visit theย Firebase consoleย and sign in with a Gmail account.

Create a Firebase project and select the </> icon to create a new Firebase web app.

Provide your app name and register it.

Install the Firebase SDK in your Next.js project by running the code snippet below.

npm install firebase

Create a firebase.ts file at the root of your Next.js project and copy the Firebase configuration code for your app into the file.

import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth, GoogleAuthProvider, GithubAuthProvider } from "firebase/auth";

const firebaseConfig = {
	apiKey: "******",
	authDomain: "**********",
	projectId: "********",
	storageBucket: "******",
	messagingSenderId: "**********",
	appId: "********",
	measurementId: "********",
};
//๐Ÿ‘‡๐Ÿป gets the app config
const app =
	getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
//๐Ÿ‘‡๐Ÿป creates a Firebase Firestore instance
const db = getFirestore(app);
//๐Ÿ‘‡๐Ÿป creates a Firebase Auth instance
const auth = getAuth(app);
//๐Ÿ‘‡๐Ÿป creates a Google & GitHub Auth instance
const googleProvider = new GoogleAuthProvider();
const githubProvider = new GithubAuthProvider();
//๐Ÿ‘‡๐Ÿป export them for use within the application
export { db, auth, googleProvider, githubProvider };

Before you can add Firebase Authentication to your application, you need to set it up in your Firebase console.

To do this, navigate to the left-hand side panel, select Build, and then click on Authentication to add Firebase Authentication to your project.

Lastly, enable the Google and Email/Password authentication methods.

Add Google authentication to Next.js

First, create a util.ts file within the app folder and copy the provided code snippet into the file.

import { signInWithPopup } from "firebase/auth";
import { auth } from "../../firebase";

//๐Ÿ‘‡๐Ÿป Split full name into first name and last name
export const splitFullName = (fullName: string): [string, string] => {
	const [firstName, ...lastNamePart] = fullName.split(" ");
	return [firstName, lastNamePart.join(" ")];
};

//๐Ÿ‘‡๐Ÿป Handle Sign in with Google and Github
export const handleSignIn = (provider: any, authProvider: any) => {
	signInWithPopup(auth, provider)
		.then((result) => {
			const credential = authProvider.credentialFromResult(result);
			const token = credential?.accessToken;
			if (token) {
				const user = result.user;
				const [first_name, last_name] = splitFullName(user.displayName!);
				console.log([first_name, last_name, user.email]);
			}
		})
		.catch((error) => {
			const errorCode = error.code;
			const errorMessage = error.message;
			console.error({ errorCode, errorMessage });
			alert(`An error occurred, ${errorMessage}`);
		});
};

The code snippet enables users to sign into the application via Google or GitHub authentication. It accepts authentication providers as parameters and logs the userโ€™s email, first name, and last name to the console after a successful authentication process.

Next, you can execute the handleSignIn function within the app/page.tsx file when a user clicks the Sign Up with Google or GitHub buttons.

import { GoogleAuthProvider, GithubAuthProvider } from "firebase/auth";
import { googleProvider, githubProvider } from "../../firebase";
import { handleSignIn } from "./util";

const handleGoogleSignIn = () => {
	setLoading(true);
	handleSignIn(googleProvider, GoogleAuthProvider);
};

const handleGithubSignIn = () => {
	setLoading(true);
	handleSignIn(githubProvider, GithubAuthProvider);
};

Add GitHub authentication to Next.js

Before the GitHub authentication method can work as expected, you need toย create a GitHub OAuth App.

Once youโ€™ve created the GitHub OAuth App, copy the client ID and client secret from the app settings. Youโ€™ll need these credentials to activate the GitHub authentication method within the application.

Go back to the Firebase Console. Enable the GitHub sign-in method and paste the client ID and secret into the input fields provided. Additionally, add the authorisation callback URL to your GitHub app.

Congratulations! Youโ€™ve successfully added GitHub and Google authentication methods to the application. Next, letโ€™s add the Email/Password authentication for Admin users.

Add email and password authentication to Next.js

Before we proceed, ensure you have enabled the email/password sign-in method within the Firebase project. Then, execute the functions below when a user signs up and logs in as an admin user.

import {
	createUserWithEmailAndPassword,
	signInWithEmailAndPassword,
} from "firebase/auth";
import { auth } from "../../firebase";

//๐Ÿ‘‡๐Ÿป Admin Firebase Sign Up Function
export const adminSignUp = async (email: string, password: string) => {
	try {
		const userCredential = await createUserWithEmailAndPassword(
			auth,
			email,
			password
		);
		const user = userCredential.user;
		if (user) {
			//๐Ÿ‘‰๐Ÿป sign up successful
		}
	} catch (e) {
		console.error(e);
		alert("Encountered an error, please try again");
	}
};

//๐Ÿ‘‡๐Ÿป Admin Firebase Login Function
export const adminLogin = async (email: string, password: string) => {
	try {
		const userCredential = await signInWithEmailAndPassword(
			auth,
			email,
			password
		);
		const user = userCredential.user;
		if (user) {
			//๐Ÿ‘‰๐Ÿป log in successful
		}
	} catch (e) {
		console.error(e);
		alert("Encountered an error, please try again");
	}
};

The provided code snippets include functions for admin user sign-up (adminSignUp) and login (adminLogin) using Firebase authentication. You can trigger these functions when a user signs up or logs in as an admin user.

You can sign users (admin and subscribers) out of the application using the code snippet below.

import { signOut } from "firebase/auth";
import { auth } from "../../firebase";

//๐Ÿ‘‡๐Ÿป Firebase Logout Function
export const adminLogOut = async () => {
	signOut(auth)
		.then(() => {
			//๐Ÿ‘‰๐Ÿป sign out successful
		})
		.catch((error) => {
			console.error({ error });
			alert("An error occurred, please try again");
		});
};

Congratulations! Youโ€™ve completed the authentication process for the application. You can read through theย concise documentationย if you encounter any issues.


How to interact with Firebase Firestore

In this section, youโ€™ll learn how to save and retrieve data from Firebase Firestore by saving newsletters, subscribers, and admin users to the database.

Before we proceed, you need to add the Firebase Firestore to your Firebase project.

Create the database in test mode, and pick your closest region.

After creating your database, select Rules from the top menu bar, edit the rules, and publish the changes. This enables you to make requests to the database for a longer period of time.

Congratulations!๐ŸŽ‰ Your Firestore database is ready.

Saving subscribersโ€™ data to the database

After a user subscribes to the newsletter, you need to save their first name, last name, email, and user ID to Firebase. To do this, execute the saveToFirebase function after a subscriber successfully signs in via the GitHub or Google sign-in method.

import { auth, db } from "../../firebase";
import { addDoc, collection, getDocs } from "firebase/firestore";

export type SubscribersData = {
	firstName: string;
	lastName: string;
	email: string;
	id: string;
};

//๐Ÿ‘‡๐Ÿป Save the subscriber data to Firebase
const saveToFirebase = async (subscriberData: SubscribersData) => {
	try {
		//๐Ÿ‘‡๐Ÿป checks if the subscriber already exists
		const querySnapshot = await getDocs(collection(db, "subscribers"));
		querySnapshot.forEach((doc) => {
			const data = doc.data();
			if (data.email === subscriberData.email) {
				window.location.href = "/subscribe";
				return;
			}
		});
		//๐Ÿ‘‡๐Ÿป saves the subscriber details
		const docRef = await addDoc(collection(db, "subscribers"), subscriberData);
		if (docRef.id) {
			window.location.href = "/subscribe";
		}
	} catch (e) {
		console.error("Error adding document: ", e);
	}
};

The saveToFirebase function validates if the user is not an existing subscriber to prevent duplicate entries before saving the subscriberโ€™s data to the database.

To differentiate between existing subscribers and admin users within the application, you can save admin users to the database.

Execute the provided function below when a user signs up as an admin.

import { db } from "../../firebase";
import { addDoc, collection } from "firebase/firestore";

//๐Ÿ‘‡๐Ÿป Add Admin to Firebase Database
const saveAdmin = async (email: string, uid: string) => {
	try {
		const docRef = await addDoc(collection(db, "admins"), { email, uid });
		if (docRef.id) {
			alert("Sign up successful!");
			window.location.href = "/admin/dashboard";
		}
	} catch (e) {
		console.error("Error adding document: ", e);
	}
};

Next, update the admin log-in function to ensure that the userโ€™s data exists within the admin collection before granting access to the application.

import { auth, db } from "../../firebase";
import { addDoc, collection, getDocs, where, query } from "firebase/firestore";

//๐Ÿ‘‡๐Ÿป Admin Firebase Login Function
export const adminLogin = async (email: string, password: string) => {
	try {
		const userCredential = await signInWithEmailAndPassword(
			auth,
			email,
			password
		);
		const user = userCredential.user;
		//๐Ÿ‘‡๐Ÿป Email/Password sign in successful
		if (user) {
			//๐Ÿ‘‡๐Ÿป check if the user exists within the database
			const q = query(collection(db, "admins"), where("uid", "==", user.uid));
			const querySnapshot = await getDocs(q);
			const data = [];
			querySnapshot.forEach((doc) => {
				data.push(doc.data());
			});
			if (data.length) {
				window.location.href = "/admin/dashboard";
				alert("Log in successful!");
			}
		}
	} catch (e) {
		console.error(e);
		alert("Encountered an error, please try again");
	}
};

To ensure that only authenticated users can access the Admin dashboard, Firebase allows you to retrieve the current userโ€™s data at any point within the application. This enables us to protect the Dashboard page from unauthorised access and listen to changes in the userโ€™s authentication state.

import { onAuthStateChanged } from "firebase/auth";
import { auth } from "../firebase";

export default function Dashboard() {
	const router = useRouter();

	useEffect(() => {
		onAuthStateChanged(auth, (user) => {
			if (!user) {
				//๐Ÿ‘‰๐Ÿป redirect user to log in form
				router.push("/");
			}
		});
	}, [router]);
}

Congratulations on making it thus far! ๐ŸŽ‰ In the upcoming sections, youโ€™ll learn how to create and send beautifully designed newsletters to subscribers using Novu and React Email.

Link to part 2

Related Posts

category: How to

Case Study: How Novu Migrated User Management to Clerk

Discover how Novu implemented Clerk for user management, enabling features like SAML SSO, OAuth providers, and multi-factor authentication. Learn about the challenges faced and the innovative solutions developed by our team. This case study provides insights into our process, integration strategies that made it possible.

Emil Pearce
Emil Pearce
category: How to

How to Implement React Notifications โ€” Including Examples, Alerts, and Libraries

Learn how to implement effective notifications in React applications. Explore the differences between stateless and stateful notifications, and discover the best libraries and practices for enhancing user engagement and information delivery in your React projects.

Emil Pearce
Emil Pearce
category: How to

How Product-Development Friction Ruins User Experience with Notifications

Discover how friction between product and development teams can lead to poor notifications and damage your user experience. Learn strategies to overcome these challenges and enhance your notification system for better user engagement.

KMac Damaso
KMac Damaso