Hi everyone, in this tutorial, we are going to fire up an authentication system using React, Firebase, and Express. We will also use Tailwind to create the user interface.
Table of Contents
- Introduction
- React and Tailwind Set Up
- Firebase Set Up
- Firebase and React Integration
- Enable Dark Mode
- Navigation
- Private Route
- Express Server
- Connect with React
- Final Thoughts
Introduction
To set up a registration/authentication system, we will leverage Firebase, a Backend-as-a-Service (Baas). What’s a BaaS?
Cloud computing has transformed the way we use technology. The BaaS cloud service model allows developers to focus on the frontend of an application by abstracting away all the stuff that goes behind it. Through BaaS, developers outsource pre-written software for different activities such as user authentication, database management, cloud storage, hosting, push notifications, and so on. We are going to use the user authentication service provided by Firebase for this project.
Here is the workflow:
- React will send a request to register and login a user to Firebase.
- Firebase will give a token to React
- React will use that token to send subsequent requests to an Express server.
- The Express server will verify the token using the Firebase admin SDK and sends a response back to React.
Quick Note:
- Technically you don’t need Express for this if you aren’t going to use a backend for your project. However, in our case, we need an Express server to later create the chat API endpoints and connect with MongoDB.
- Also, if you encounter any issues throughout the tutorial, you can check out the code in the GitHub repository.
React and Tailwind Set Up
Let's create a React application first. Type in the following to create a React app.
npx create-react-app frontend
Remove git since we won’t use it inside the frontend app. If you are on Linux, you can remove it by running this:
cd frontend
rm -rf .git
Alright, we are going to use Tailwind CSS to create the components, so let’s install it.
Make sure you are inside the frontend folder, open up your terminal and install Tailwind CSS
# Installs tailwindcss and its dependencies
npm install -D tailwindcss postcss autoprefixer
# Generates tailwind.config.js and postcss.config.js files
npx tailwindcss init -p
Open tailwind.config.js
and configure your template paths.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
Open index.css
, remove the content and add the following Tailwind directives. Check this out to learn more about these Tailwind-specific at-rules.
@tailwind base;
@tailwind components;
@tailwind utilities;
Now, let’s delete files we won’t be using. The final structure should look something like this:
Your index.js
file should look like this:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
And finally to make sure everything is set up correctly, add the following to App.js
and start the project.
// App.js
function App() {
return (
<div>
<h1 className="text-3xl font-bold underline">Hello world!</h1>
</div>
);
}
export default App;
Start the project:
npm run start
Perfect! now inside the src
folder, create another folder named components
. Then, inside components
, create a folder named accounts
. We will put user auth-related components inside it.
Inside accounts
, create 2 files named Register.js
and Login.js
For now, put a boilerplate like the following. If you are on VSCode, you can generate this by using the rfc
shortcut.
// Register.js
import React from "react";
export default function Register() {
return <div>Register</div>;
}
// Login.js
import React from "react";
export default function Login() {
return <div>Login</div>;
}
React Router
We will be using React router V6 for routing between different pages. Let’s go ahead and install it.
npm i react-router-dom
Open App.js
and create the path for login and register.
// App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Register from "./components/accounts/Register";
import Login from "./components/accounts/Login";
function App() {
return (
<Router>
<Routes>
<Route exact path="/register" element={<Register />} />
<Route exact path="/login" element={<Login />} />
</Routes>
</Router>
);
}
export default App;
Go to your browser and test the routes: http://localhost:3000/login and http://localhost:3000/register
Register and Login Components
Up until this point, we have been doing basic configuration. Let’s now get to creating the UI for account registration and login.
// Register.js
import { Link } from "react-router-dom";
export default function Register() {
return (
<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-4 text-3xl text-center tracking-tight font-light dark:text-white">
Register your account
</h2>
</div>
<form className="mt-8 space-y-6">
<div className="rounded-md shadow-sm -space-y-px">
<div>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
</div>
<div>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
<div>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div>
<button
type="submit"
className=" w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-sky-800 hover:bg-sky-900"
>
Register
</button>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<Link
to="/login"
className="text-blue-600 hover:underline dark:text-blue-500"
>
Already have an account? Login
</Link>
</div>
</div>
</form>
</div>
</div>
);
}
- We have 3 fields. One for Email, the other two for password and password confirmation respectively. And for
Login.js
we have 2 fields; Email and password.
// Login.js
import { Link } from "react-router-dom";
export default function Login() {
return (
<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-4 text-3xl text-center tracking-tight font-light dark:text-white">
Login to your account
</h2>
</div>
<form className="mt-8 space-y-6">
<div className="rounded-md shadow-sm -space-y-px">
<div>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
</div>
<div>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div>
<button
type="submit"
className=" w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-sky-800 hover:bg-sky-900"
>
Login
</button>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<Link
to="/register"
className="text-blue-600 hover:underline dark:text-blue-500"
>
Don't have an account? Register
</Link>
</div>
</div>
</form>
</div>
</div>
);
}
Don’t worry about the classes of the elements that are prefixed with dark
. We will talk about them later when we enable dark mode.
Let’s now use the useState
hook to store the form data. Inside Register.js
, import useState
at the top and add the following 3 states.
import { useState } from "react";
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
Then, add an onChange
event listener to the input fields as follows.
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => setConfirmPassword(e.target.value)}
We now need a function that will handle the form submission.
async function handleFormSubmit(e) {
e.preventDefault();
// Here We will get form values and
// invoke a function that will register the user
}
Add the function as an onSubmit
event listener to the form.
onSubmit={handleFormSubmit}
Do the same thing for Login.js
. The only difference is that we don’t have a confirm password input field. The rest is the same. Now that our forms are ready, let’s move our attention to Firebase.
Firebase Set Up
If it’s your first time using Firebase, follow the following steps to create a project. Otherwise, you can move to the next section.
We first need to create a Firebase project using the Firebase console, so Log in and then click Create a project.
Give any name you like for the project and click Continue. You will then be prompted to enable Google Analytics for your project (optional). We don’t need that.
Once you have completed the steps, you will be presented with a Dashboard. Click on Authentication from the navigation panel and go to the Sign-in Method tab. Enable Email/Password method since that’s what we are going to use.
Firebase and React Integration
For React to communicate with Firebase services, we need to install the firebase
package.
npm i firebase
After you have created a new project in Firebase, you need to create an application. Go to your Firebase Console dashboard, click on Project Settings, and select this icon (</>) to add Firebase to your web app. Doing so will give you config data.
Create a .env
file inside the root directory of your project and store all these config data in it.
// .env
REACT_APP_FIREBASE_API_KEY =
REACT_APP_FIREBASE_AUTH_DOMAIN =
REACT_APP_FIREBASE_PROJECT_ID =
REACT_APP_FIREBASE_STORAGE_BUCKET =
REACT_APP_FIREBASE_MESSAGING_SENDER_ID =
REACT_APP_FIREBASE_APP_ID =
- When setting up environment variables, you should prefix them with
REACT_APP_
. In addition, make sure to put the.env
file in your root folder. Finally, restart your server for the changes to take place.
Inside the src
folder, create a folder called config
and inside it, create a file called firebase.js
This is where we will set up Firebase.
// firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
// gives us an auth instance
const auth = getAuth(app);
// in order to use this auth instance elsewhere
export default auth;
Now that we have Firebase setup, we can start doing authentication.
React Context
We are going to use React context to set up the authentication because we want to access the current user everywhere in our app. If you aren’t familiar with React context, don’t worry. We are going to walk through it together.
React context allows us to share data (state) across our components more easily. This is great when you are passing data that can be used in any component of your application.
Inside the src
folder, create a folder named contexts
and inside it, create a file named AuthContext.js
and put the following to set up context.
// AuthContext.js
import { createContext, useContext } from "react";
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const value = {};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
What is going on? Well, let me explain
-
const AuthContext = createContext()
will return a Consumer and a Provider component. Provider provides state to its children. It takes in thevalue
prop and passes it down to its children components that might need to access them. The Consumer consumes and uses the state passed down to it by the Provider. - In the above case, the component that will wrap the Provider is
AuthProvider
We also exported it, so that we can use it elsewhere. - The custom
useAuth
hook will allow us to consume context in our application by returning auseContext
instance ofAuthContext
That is the basic setup for context. Let’s now add Firebase functions to register, login, and find the current user.
First, import createUserWithEmailAndPassword
and signInWithEmailAndPassword
that Firebase provides for user registration and login. Also, import auth
from our earlier Firebase configuration.
// AuthContext.js
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from "firebase/auth";
import auth from "../config/firebase";
Users can create a new account by passing their email address and password. The functions return a Promise and we will consume that inside the register and login components.
function register(email, password) {
// If the new account was created, the user is signed in automatically.
return createUserWithEmailAndPassword(auth, email, password);
}
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
Alright, but how can we get the signed-in user details? We can set an observer on the auth
object. Before that, however, let’s first create a state that will store the current user and pass it down in the value
props.
import { useState } from "react";
const [currentUser, setCurrentUser] = useState();
const value = {
currentUser,
};
We can now set the onAuthStateChanged
observer on the auth
object. This will notify us when the user is signed in and update the state.
import { useEffect } from "react";
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
- The observer i.e.
onAuthStateChanged
is put inside auseEffect
because we only want to render it once i.e. when the component mounts. When the component un-mounts, we unsubscribe from the event. - The
loading
state is initially set totrue
. When Firebase finishes its job and updates the user, we set it to false. This will prevent the children components from rendering while the action is taking place.
The final code for AuthContext.js
should look like this:
// AuthContext.js
import { createContext, useContext, useState, useEffect } from "react";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from "firebase/auth";
import auth from "../config/firebase";
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function register(email, password) {
return createUserWithEmailAndPassword(auth, email, password);
}
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
login,
register,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
Let’s now wrap our components inside of the AuthProvider
so that we have access to the context.
// App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./contexts/AuthContext"; // import this
import Register from "./components/accounts/Register";
import Login from "./components/accounts/Login";
function App() {
return (
<AuthProvider> // Wrap everything inside this provider
<Router>
<Routes>
<Route exact path="/register" element={<Register />} />
<Route exact path="/login" element={<Login />} />
</Routes>
</Router>
</AuthProvider>
);
}
export default App;
Now, to use the register
and currentUser
context, head over to Register.js
and import the useAuth
hook.
// Register.js
import { useAuth } from "../../contexts/AuthContext";
Then, import useEffect
and useNavigate
and add the following:
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
const { currentUser, register } = useAuth();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (currentUser) {
navigate("/");
}
}, [currentUser, navigate]);
-
const navigate = useNavigate();
React router hook that returns a function to navigate programmatically. -
const { currentUser, register } = useAuth();
gets thecurrentUser
andregister
contexts since those are what we will be using. - The
loading
state will be used to disable the button when the registration action is executing. - The
useEffect
hook is in place to prevent users from accessing the registration page while they are authenticated. It will redirect them to the home page if they try to do so.
Then inside the handleFormSubmit
function we created earlier, we are going to handle the registration logic:
async function handleFormSubmit(e) {
e.preventDefault();
if (password !== confirmPassword) {
return alert("Passwords do not match");
}
try {
setLoading(true);
await register(email, password);
navigate("/profile");
} catch (e) {
alert("Failed to register");
}
setLoading(false);
}
- First, if the passwords don’t match we exit out of the function by alerting the error. For now, we are not managing the errors. Later, however, we will handle them using a state.
- The
register
function returns a promise, so we are using theawait
keyword to wait for the call to be settled. And we are using thetry…catch
block for error handling. If the call is successful, the user will be redirected to the profile page (we haven’t created it yet). However, the flow will be sent to thecatch
block if it fails.
To disable the submit button when the action is executing, add disabled={loading}
to the submit Button
.
Cool! let’s test what we have so far. Start your server and go to http://localhost:3000/register and fill in the form with passwords that don’t match. You should get the following alert:
Now, fill in the correct credentials and you should be redirected to http://localhost:3000/profile. However, since we haven’t created this route yet, nothing will be displayed.
Do the same thing for the Login.js
component. You only need to change the register
call to login
and remove the password confirmation check.
Error Handling
We want to use a single component that will handle and display errors (if there are any). For this, let’s use context.
Go to AuthContext.js
and create this state:
const [error, setError] = useState("");
Add it to the value
props:
const value = {
error,
setError,
};
Inside components, create a folder named layouts
. And inside it, create a file called ErrorMessage.js
We will use this component to display error messages from anywhere in our app.
First, let’s install herocions to use the X
icon in our alert.
npm i @heroicons/react@v1
Then inside ErrorMessage.js
:
// ErrorMessage.js
import { XCircleIcon } from "@heroicons/react/solid";
import { useAuth } from "../../contexts/AuthContext";
export default function ErrorMessage() {
const { error, setError } = useAuth();
return (
error && (
<div className="flex justify-center">
<div className="rounded-md max-w-md w-full bg-red-50 p-4 mt-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
onClick={() => setError("")}
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
Error: {error}
</h3>
</div>
</div>
</div>
</div>
)
);
}
- The above snippet checks if there is an error and displays it. When the
X
icon is clicked it will set the error to an empty string.
Then render this component inside App.js
at the top.
import ErrorMessage from "./components/layouts/ErrorMessage"; // add this import
<AuthProvider>
<Router>
<ErrorMessage /> // add this
<Routes>
<Route exact path="/register" element={<Register />} />
<Route exact path="/login" element={<Login />} />
</Routes>
</Router>
</AuthProvider>
Now, let’s update Register.js
to set the errors accordingly.
const { currentUser, register, setError } = useAuth(); // Get setError as well
async function handleFormSubmit(e) {
e.preventDefault();
if (password !== confirmPassword) {
return setError("Passwords do not match"); // Replace the alert with this
}
try {
setError(""); // Remove error when trying to register
setLoading(true);
await register(email, password);
navigate("/profile");
} catch (e) {
setError("Failed to register"); // Replace the alert with this
}
setLoading(false);
}
Do the same thing for Login.js
and test it. Before testing it though, make sure to comment out the useEffect
hook that checks if there is a current user so that it won’t redirect you to the home page.
You should get something like this:
Profile
Now that we have registration and login set up, let’s create a route to set a name and avatar for users.
The Firebase user object has basic properties such as displayName
, photoURL
, phoneNumber
, etc. You can check this by logging the currentUser
object on the console. We are going to update the user’s profile by using the updateProfile
function from Firebase.
Open AuthContext.js
and import this function:
import { updateProfile } from "firebase/auth";
Add the following function alongside register
and login
function updateUserProfile(user, profile) {
return updateProfile(user, profile);
}
- The
updateProfile
function takes a user and any user information we need to update.
Add updateUserProfile
in value
props.
const value = {
// ...
updateUserProfile,
};
We are going to update the display name and photo of the user. To generate cool avatars, we are going to use this API.
- DiceBear uses the following URL as a source to generate images. https://avatars.dicebear.com/api/:sprites/:seed.svg
You can replace
:sprites
withmale
,female
,human
,identicon
,initials
,bottts
,avataaars
,jdenticon
,gridy
ormicah
. The value of:seed
can be anything you like but don't use any sensitive or personal data here!
Inside the src
folder, create a folder called utils
. And inside it, create a file called GenerateAvatar.js
// GenerateAvatar.js
const generateDiceBearAvataaars = (seed) =>
`https://avatars.dicebear.com/api/avataaars/${seed}.svg`;
const generateDiceBearBottts = (seed) =>
`https://avatars.dicebear.com/api/bottts/${seed}.svg`;
const generateDiceBearGridy = (seed) =>
`https://avatars.dicebear.com/api/gridy/${seed}.svg`;
export const generateAvatar = () => {
const data = [];
for (let i = 0; i < 2; i++) {
const res = generateDiceBearAvataaars(Math.random());
data.push(res);
}
for (let i = 0; i < 2; i++) {
const res = generateDiceBearBottts(Math.random());
data.push(res);
}
for (let i = 0; i < 2; i++) {
const res = generateDiceBearGridy(Math.random());
data.push(res);
}
return data;
};
- The above function generates 6 random avatars and returns the array. We have exported the function so that we can use it elsewhere.
Now, create a file called Profile.js
inside accounts
. Most of the things in this file are going to be similar to Register.js
and Login.js
.
// Profile.js
import { generateAvatar } from "../../utils/GenerateAvatar";
const [avatars, setAvatars] = useState([]);
useEffect(() => {
const fetchData = () => {
const res = generateAvatar();
setAvatars(res);
};
fetchData();
}, []);
- This will generate avatars using the utility function we created earlier and set them to the
avatars
state when the component mounts.
We will also have the following state to track the selected avatar.
const [selectedAvatar, setSelectedAvatar] = useState();
This is what Profile.js
looks like now
// Profile.js
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";
import { generateAvatar } from "../../utils/GenerateAvatar";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
export default function Profile() {
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [avatars, setAvatars] = useState([]);
const [selectedAvatar, setSelectedAvatar] = useState();
const [loading, setLoading] = useState(false);
const { currentUser, updateUserProfile, setError } = useAuth();
useEffect(() => {
const fetchData = () => {
const res = generateAvatar();
setAvatars(res);
};
fetchData();
}, []);
const handleFormSubmit = async (e) => {
e.preventDefault();
if (selectedAvatar === undefined) {
return setError("Please select an avatar");
}
try {
setError("");
setLoading(true);
const user = currentUser;
const profile = {
displayName: username,
photoURL: avatars[selectedAvatar],
};
await updateUserProfile(user, profile);
navigate("/");
} catch (e) {
setError("Failed to update profile");
}
setLoading(false);
};
return (
<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<h2 className="mt-4 text-3xl text-center tracking-tight font-light dark:text-white">
Pick an avatar
</h2>
</div>
<form className="space-y-6" onSubmit={handleFormSubmit}>
<div className="flex flex-wrap -m-1 md:-m-2">
{avatars.map((avatar, index) => (
<div key={index} className="flex flex-wrap w-1/3">
<div className="w-full p-1 md:p-2">
<img
alt="gallery"
className={classNames(
index === selectedAvatar
? "border-4 border-blue-700 dark:border-blue-700"
: "cursor-pointer hover:border-4 hover:border-blue-700",
"block object-cover object-center w-36 h-36 rounded-full"
)}
src={avatar}
onClick={() => setSelectedAvatar(index)}
/>
</div>
</div>
))}
</div>
<div className="rounded-md shadow-sm -space-y-px">
<input
id="username"
name="username"
type="text"
autoComplete="username"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Enter a Display Name"
defaultValue={currentUser.displayName && currentUser.displayName}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<button
type="submit"
disabled={loading}
className="w-full py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Update Profile
</button>
</div>
</form>
</div>
</div>
);
}
Now, let’s create the route in App.js
import Profile from "./components/accounts/Profile"; // Add this to the imports list
<Route exact path="/profile" element={<Profile />} /> // Add this route
Finally, go to http://localhost:3000/profile and check if everything is working. You will get something like this.
When you update the profile, it will redirect you to a blank page, don’t worry we haven’t created a home page yet.
Logout
First, let’s add the Firebase function that will allow us to log out users. Inside your AuthContext.js
, add the following
import { signOut } from "firebase/auth"; // Add this import
function logout() {
return signOut(auth);
}
Simple as that!
Then create a file called Logout.js
inside the accounts
folder. We are going to create a modal that will ask for the user’s confirmation to log out and fires the logout
function. We are going to use the Dialog
and Transition
components from Headless UI so let’s install it first
npm i @headlessui/react
// Logout.js
import { Fragment, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Dialog, Transition } from "@headlessui/react";
import { ExclamationIcon } from "@heroicons/react/outline";
import { useAuth } from "../../contexts/AuthContext";
export default function Logout({ modal, setModal }) {
const cancelButtonRef = useRef(null);
const navigate = useNavigate();
const { logout, setError } = useAuth();
async function handleLogout() {
try {
setError("");
await logout();
setModal(false);
navigate("/login");
} catch {
setError("Failed to logout");
}
}
return (
<Transition.Root show={modal} as={Fragment}>
<Dialog
as="div"
className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={cancelButtonRef}
onClose={setModal}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
​
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white dark:bg-gray-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-gray-200 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg leading-6 font-medium text-gray-500 dark:text-gray-400"
>
Logging out
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500 dark:text-gray-400">
Are you sure you want to log out ?
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={handleLogout}
>
Logout
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center shadow-sm px-4 py-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm
rounded-md border border-gray-300 bg-white text-gray-500 text-base font-medium hover:bg-gray-100 focus:outline-none focus:ring-gray-200 focus:ring-2 focus:ring-offset-2 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600
"
onClick={() => setModal(false)}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
}
Done!
Enable Dark Mode
If you have been paying attention to the Tailwind classes of our component elements, you must have seen something like dark:text-white
This kind of style pattern is applied to the elements when dark mode is enabled. We haven’t enabled it yet, so let’s do that now.
Open your tailwind.config.js
file and add the following:
darkMode: "class",
Inside layouts
, create a file called ThemeToggler.js
and add a button that will toggle between light and dark mode.
// ThemeToggler.js
export default function ThemeToggler() {
return (
<button
ref={themeToggleBtn}
type="button"
className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
onClick={() => handleThemeToggle()}
>
<svg
ref={themeToggleDarkIcon}
className="hidden w-8 h-8"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg
ref={themeToggleLightIcon}
className="hidden w-8 h-8"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
);
}
Create a useRef
hook to access the above theme toggle button, theme toggle light icon, and theme toggle dark icon.
import { useRef } from "react";
const themeToggleBtn = useRef();
const themeToggleLightIcon = useRef();
const themeToggleDarkIcon = useRef();
Add refs
to the toggle button, dark icon, and light icon SVGs respectively like this:
ref={themeToggleBtn}
ref={themeToggleDarkIcon}
ref={themeToggleLightIcon}
For the button, add an onClick
event listener:
onClick={() => handleThemeToggle()}
Let’s now create this function. When the button is pressed it will check whether or not the user has a color-theme set in local storage previously, add/remove the dark class, and update local storage.
const handleThemeToggle = () => {
themeToggleDarkIcon.current.classList.toggle("hidden");
themeToggleLightIcon.current.classList.toggle("hidden");
if (localStorage.getItem("color-theme")) {
if (localStorage.getItem("color-theme") === "light") {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
}
} else {
if (document.documentElement.classList.contains("dark")) {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
} else {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
}
}
};
Next, when the component renders, we want to either set the dark or light mode by checking previous user preference or settings.
useEffect(() => {
if (
localStorage.getItem("color-theme") === "dark" ||
(!("color-theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
themeToggleLightIcon.current.classList.remove("hidden");
} else {
document.documentElement.classList.remove("dark");
themeToggleDarkIcon.current.classList.remove("hidden");
}
}, []);
Final code for ThemeToggler.js
// ThemeToggler.js
import { useEffect, useRef } from "react";
export default function ThemeToggler() {
const themeToggleBtn = useRef();
const themeToggleLightIcon = useRef();
const themeToggleDarkIcon = useRef();
useEffect(() => {
if (
localStorage.getItem("color-theme") === "dark" ||
(!("color-theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
themeToggleLightIcon.current.classList.remove("hidden");
} else {
document.documentElement.classList.remove("dark");
themeToggleDarkIcon.current.classList.remove("hidden");
}
}, []);
const handleThemeToggle = () => {
themeToggleDarkIcon.current.classList.toggle("hidden");
themeToggleLightIcon.current.classList.toggle("hidden");
if (localStorage.getItem("color-theme")) {
if (localStorage.getItem("color-theme") === "light") {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
}
} else {
if (document.documentElement.classList.contains("dark")) {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
} else {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
}
}
};
return (
<button
ref={themeToggleBtn}
type="button"
className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
onClick={() => handleThemeToggle()}
>
<svg
ref={themeToggleDarkIcon}
className="hidden w-8 h-8"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg
ref={themeToggleLightIcon}
className="hidden w-8 h-8"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
);
}
Finally, to apply dark mode to the body of our application, add the following in index.css
. It will add our own default base styles for the body.
@layer base {
body {
@apply bg-white dark:bg-gray-900;
}
}
Let’s quickly add a navigation component and we will test the logout and dark mode functionalities.
Navigation
The navigation will contain theme toggler, logout and profile icons.
Inside layouts
create a file called Header.js
// Header.js
import { LogoutIcon } from "@heroicons/react/outline";
import { useState } from "react";
import { Link } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";
import Logout from "../accounts/Logout";
import ThemeToggler from "./ThemeToggler";
export default function Header() {
const [modal, setModal] = useState(false);
const { currentUser } = useAuth();
return (
<>
<nav className="px- px-2 sm:px-4 py-2.5 bg-gray-50 border-gray-200 dark:bg-gray-800 dark:border-gray-700 text-gray-900 text-sm rounded border dark:text-white">
<div className="container mx-auto flex flex-wrap items-center justify-between">
<Link to="/" className="flex">
<span className="self-center text-lg font-semibold whitespace-nowrap text-gray-900 dark:text-white">
Chat App
</span>
</Link>
<div className="flex md:order-2">
<ThemeToggler />
{currentUser && (
<>
<button
className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
onClick={() => setModal(true)}
>
<LogoutIcon className="h-8 w-8" aria-hidden="true" />
</button>
<Link
to="/profile"
className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-full text-sm p-2.5"
>
<img
className="h-8 w-8 rounded-full"
src={currentUser.photoURL}
alt=""
/>
</Link>
</>
)}
</div>
</div>
</nav>
{modal && <Logout modal={modal} setModal={setModal} />}
</>
);
}
- We used conditional rendering to display the logout and profile icons only when the user is authenticated.
- The modal state will be used to render the
Logout
modal for confirmation.
Finally, let’s render Header.js
inside App.js
// App.js
import Header from "./components/layouts/Header"; // Add this
<AuthProvider>
<Router>
<Header /> // And this
<ErrorMessage />
<Routes>
<Route exact path="/register" element={<Register />} />
<Route exact path="/login" element={<Login />} />
<Route exact path="/profile" element={<Profile />} />
</Routes>
</Router>
</AuthProvider>
To test this go to http://localhost:3000/profile. You will get something like this when dark mode is enabled.
The logout modal (with light mode):
Great! moving on to private routes.
Private Route
Routes such as /profile
should only be accessed if the user is logged in. To make such components private, let’s go ahead and create a higher-order component that will wrap them.
Inside the utils
folder, create a file called WithPrivateRoute.js
// WithPrivateRoute.js
import { Navigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
const WithPrivateRoute = ({ children }) => {
const { currentUser } = useAuth();
// If there is a current user it will render the passed down component
if (currentUser) {
return children;
}
// Otherwise redirect to the login route
return <Navigate to="/login" />;
};
export default WithPrivateRoute;
We can now use this higher-order component to wrap Profile.js
The final look of App.js
:
// App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./contexts/AuthContext";
import Register from "./components/accounts/Register";
import Login from "./components/accounts/Login";
import Profile from "./components/accounts/Profile";
import WithPrivateRoute from "./utils/WithPrivateRoute";
import Header from "./components/layouts/Header";
import ErrorMessage from "./components/layouts/ErrorMessage";
function App() {
return (
<AuthProvider>
<Router>
<Header />
<ErrorMessage />
<Routes>
<Route exact path="/register" element={<Register />} />
<Route exact path="/login" element={<Login />} />
<Route
exact
path="/profile"
element={
<WithPrivateRoute>
<Profile />
</WithPrivateRoute>
}
/>
</Routes>
</Router>
</AuthProvider>
);
}
export default App;
You can test this by trying to access the profile page without logging in.
This marks the end of the frontend section for authentication. Let’s now get started with Express to verify users for subsequent requests.
Express Server
Express Boilerplate
The backend is going to be an independent app, so in a new terminal type in the following:
npm init -y
This will create a package.json
file
Open it up and let’s modify a couple of things.
First, we are going to use ES 6 modules (import syntax), so let’s enable it.
"type": "module",
We are also going to use nodemon so that we don’t need to restart our server every time we make changes. If you haven’t already, install it globally:
npm install -g nodemon
Then add the following to the scripts (we will create server/index.js
next)
"start": "nodemon server/index.js"
Your package.json
should look like this
{
"name": "chat-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon server/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.1"
}
}
Now install Express:
npm i express
Create a folder named server
, cd
into it, and create index.js
file. This is where we will set up our server.
mkdir server
cd server
touch index.js
Inside index.js
let’s add a simple endpoint to fire up and test our server.
// index.js
import express from "express";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get("/", (req, res) => {
res.send("working fine");
});
const PORT = 3001;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
To test it:
cd ../
npm start
Now, if you go to http://localhost:3001/ on your browser, you should see the “working fine” output.
Enabling Cors
We need to enable cors to allow React to send requests to Express.
Install cors:
npm i cors
And inside index.js
import cors from "cors"; // Add this to the list of imports
app.use(cors()); // Use the cors middleware
Using Environment Variables
To use environment variables, let’s install dotenv
npm i dotnev
Then create a .env
file at the root of your project
touch .env
Open it and add the port
PORT=3001
Then in index.js
import dotenv
and access the PORT
variable we just created.
import dotenv from "dotenv"; // Add to import list
dotenv.config(); // Configure dotenv to access the env variables
const PORT = process.env.PORT || 8080; // Use this instead of hardcoding it like before
Connect with React
Now that we have enabled cors
, let’s test this by sending a request from React.
Go to the frontend
folder and open up Header.js
from components/layouts
. Add the following useEffect
hook. Note that this is just for testing purposes. You can get rid of it later.
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch("http://localhost:3001");
console.log(await res.text());
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
- We are making a request to this http://localhost:3001 endpoint using the built-in
fetch
API and logging the response to the console. If you head over to your React app at http://localhost:3000/, you should see the “working fine” output in the console.
Dope! React can communicate with the backend now. However, what we want to do is prevent React from getting a response if a verified user token is not sent. We will do 2 things:
- Send the Firebase token from the frontend to the backend
- Create a middleware in Express to verify the token
Sending Firebase Token to Express
Again, open Header.js
, and let’s modify the previous request to send the authentication token as a header Authorization: Bearer token
. How can we get the token? Firebase makes this really easy.
// Header.js
import auth from "../../config/firebase";
useEffect(() => {
const fetchData = async () => {
try {
const user = auth.currentUser;
const token = user && (await user.getIdToken());
const payloadHeader = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
};
const res = await fetch("http://localhost:3001", payloadHeader);
console.log(await res.text());
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
- First, we imported
auth
from our firebase config. We can use this to get the current user instance.getIdToken()
returns the token if it has not expired or if it will not expire in the next five minutes. Otherwise, this will refresh the token and return a new one.
Now if you go to http://localhost:3000/ network tab, you can see the Authorization
header being sent as well.
This is looking good. The next step is to verify the token in our backend.
Creating the Middleware
The middleware we are going to create will access the request to find the token. Then, we will verify it. How? Again, Firebase to the rescue! Firebase has a backend module that allows us to verify the token. Let’s install it.
Inside your root directory (where package.json
and frontend
folder are located):
npm i firebase-admin
According to the docs to use Firebase-admin SDK we need:
- [x] A Firebase project.
- [x] A Firebase Admin SDK service account to communicate with Firebase. This service account is created automatically when you create a Firebase project or add Firebase to a Google Cloud project.
- [ ] A configuration file with your service account's credentials.
We have done the first 2. To get the service account go to your Firebase Dashboard and open Settings in the side panel. Then click the Service Accounts tab and click Generate New Private Key, then confirm by clicking Generate Key.
Once you download the JSON
file containing the keys, go back to your project and navigate to the server
folder. Create a folder called config
and inside it, create a file called serviceAccountKey.json
. This is where we will put the generated keys.
cd server
mkdir config
cd config
touch serviceAccountKey.json
After copy-pasting the keys into this file, create another file inside this config
folder called firebase-config.js
touch firebase-config.js
Then open this file and,
//firebase-config.js
import { initializeApp, cert } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
import serviceAccountKey from "./serviceAccountKey.json" assert { type: "json" };
const app = initializeApp({
credential: cert(serviceAccountKey),
});
const auth = getAuth(app);
export default auth;
- We imported the
JSON
file with the Firebase configurations and used it to initialize the Firebase SDK. We then exportedauth
to use it elsewhere.
Next, inside the server
folder, create a folder called middleware
. Inside middleware
, create a file called VerifyToken.js
Then, open VerifyToken.js
and add the following:
// VerifyToken.js
import auth from "../config/firebase-config.js";
export const VerifyToken = async (req, res, next) => {
const token = req.headers.authorization.split(" ")[1];
try {
const decodeValue = await auth.verifyIdToken(token);
if (decodeValue) {
req.user = decodeValue;
return next();
}
} catch (e) {
return res.json({ message: "Internal Error" });
}
};
- First, we imported the Firebase
auth
we created earlier. -
const token = req.headers.authorization.split(" ")[1];
Here we are splitting the authorization header to get the token from the request. -
auth.verifyIdToken(token);
will decode the token to its associated user. Finally, we check if this decoded value is the same as the user sending the request.
Let’s now use this middleware
in index.js
import { VerifyToken } from "./middlewares/VerifyToken.js";
app.use(VerifyToken); // Add this middleware
index.js
will look like this:
// index.js
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import { VerifyToken } from "./middlewares/VerifyToken.js";
const app = express();
dotenv.config();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(VerifyToken);
const PORT = process.env.PORT || 8080;
app.get("/", (req, res) => {
res.send("working fine");
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
Now, if you go back to http://localhost:3000/ and refresh, you will see “working fine” outputted on the console. However, if you replace the token with some random value or remove the header, Express will reject the request.
Perfect! Everything is working now.
Final Thoughts
Got lost somewhere throughout the process? No problem, check out the project on GitHub or ask your question in the comments section.
Feedback or comment? let me know down below. Thanks!
Top comments (2)
Very usefull article.
In the repo, ChatServices has a method :
const getUserToken = async () => {
const user = auth.currentUser;
const token = user && (await user.getIdToken());
return token;
};
Wouldn't be better if this was outside of that class thus other future service could use it ? Any suggestion ?
const user = auth.currentUser;
const token = user && (await user.getIdToken());
i get this as null when i refresh page and my api call is not authenticated. Any solutions?