Integration of Facial Authentication (Recognition) on an Employee Dashboard using FACEIO, Next.js & Typescript.
Ekemini Samuel
Posted on May 13, 2024
While there are different authentication methods, Facial Authentication adds a higher level of security. In this tutorial, we'll integrate Facial Authentication with FACEIO to an employee dashboard sign-up page.
At the end of this project, you can sign in to the Employee dashboard and be authenticated with FACEIO Authentication.
Let's begin!
Prerequisites
- Basic understanding of Next.js and Typescript
- A FACEIO account
- A package manager installed (npm), and a Code editor (VS Code or your favourite)
What is FACEIO?
FACEIO is a facial authentication framework that can be implemented on any website with JavaScript to easily authenticate users via Face Recognition instead of using a login/password or OTP code.
Creating the UI for the Employee Dashboard Signup/Login Page
Step 1: Create a Next.JS app for the project
Open your terminal, navigate to your workspace folder, and run the command below:
npx create-next-app@latest employee-next-app
This will create an employee-next-app
directory with a Next.JS project structure. Select Yes for all the prompts (except the last one, as the default import alias is recommended) to install Typescript, TailwindCSS, and other dependencies needed for the Next.js project.
Still in your terminal, navigate to the project directory like so:
cd employee-next-app
Step 2: Install the FACEIO library for facial authentication. Read the docs to learn more.
In the employee-next-app
directory, run this command to install fio.js
npm i @faceio/fiojs
After installing the fio.js
library, run this command to open the project in your VS Code editor:
code .
In the src\app
directory, create a file named faceio_fiojs.d.ts
and enter the code below:
declare module "@faceio/fiojs"
This initializes a module to interact with the fio.js
library.
Step 3: Creating the Reusable Components
Next, we'll create components that will be used for the employee dashboard.
Create a Component
folder in the src\app
directory. Then create a new file Sidebar.tsx
and enter the code below, this will create a Sidebar component.
import React from "react";
const Sidebar = () => {
return (
<nav className="space-y-2">
<a
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-gray-200"
href="#"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-5 w-5 text-black"
>
<rect width="7" height="9" x="3" y="3" rx="1"></rect>
<rect width="7" height="5" x="14" y="3" rx="1"></rect>
<rect width="7" height="9" x="14" y="12" rx="1"></rect>
<rect width="7" height="5" x="3" y="16" rx="1"></rect>
</svg>
<span className="text-black">Dashboard</span>
</a>
<a
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-gray-200"
href="#"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-5 w-5 text-black"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span className="text-black">Team</span>
</a>
<a
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-gray-200"
href="#"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-5 w-5 text-black"
>
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path>
<path d="M14 2v4a2 2 0 0 0 2 2h4"></path>
<path d="M10 9H8"></path>
<path d="M16 13H8"></path>
<path d="M16 17H8"></path>
</svg>
<span className="text-black">HR</span>
</a>
<a
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-gray-200"
href="#"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-5 w-5 text-black"
>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<span className="text-black">Settings</span>
</a>
</nav>
);
};
export default Sidebar;
Also, create the Navigation bar component with a file called Navbar.tsx
and enter the code below, still in the Component directory:
import React from "react";
const Navbar = () => {
return (
<div>
<header className="bg-gray-900 text-white">
<div className="container mx-auto flex h-16 items-center justify-between px-4 md:px-6">
<a className="flex items-center gap-2" href="#">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-6 w-6"
>
<path d="m8 3 4 8 5-5 5 15H2L8 3z"></path>
</svg>
<span className="text-lg font-semibold">Speed Inc</span>
</a>
<nav className="hidden space-x-4 md:flex">
<a className="hover:text-gray-300" href="#">
Dashboard
</a>
<a className="hover:text-gray-300" href="#">
Team
</a>
<a className="hover:text-gray-300" href="#">
HR
</a>
<a className="hover:text-gray-300" href="#">
Settings
</a>
</nav>
<button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 w-10 md:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-6 w-6"
>
<line x1="4" x2="20" y1="12" y2="12"></line>
<line x1="4" x2="20" y1="6" y2="6"></line>
<line x1="4" x2="20" y1="18" y2="18"></line>
</svg>
<span className="sr-only">Toggle navigation</span>
</button>
</div>
</header>
</div>
);
};
export default Navbar;
Step 4: Creating the Dashboard page
Next, let's create a Dashboard page that users who are authenticated with FACEIO can access. We will import the Navigation bar and Sidebar components we created above using:
import Navbar from "../Component/Navbar";
import Sidebar from "../Component/Sidebar";
Inside the src/app
directory, create a new folder named dashboard
and a file named Page.tsx
. Enter the following code into Page.tsx
:
"use client";
import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import Navbar from "../Component/Navbar";
import Sidebar from "../Component/Sidebar";
const Page = () => {
const router = useRouter();
useEffect(() => {
const faceid = localStorage.getItem("faceId");
if (!faceid) {
router.push("/");
}
}, [history]);
// Add JSON DATA here
return (
<>
<Navbar />
<div className="flex min-h-screen flex-col">
<div className="flex flex-1">
<div className="hidden w-64 bg-gray-100 p-4 md:block">
<Sidebar />
</div>
<main className="flex-1 bg-gray-50 p-4 md:p-6">
{/* Dashboard Section */}
<section>
{/* Dashboard content */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Dashboard</h2>
<button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2">
View All
</button>
</div>
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{dashboardData.map((item, index) => (
<div
key={index}
className="rounded-lg border bg-card text-card-foreground shadow-sm"
data-v0-t="card"
>
<div className="flex flex-col space-y-1.5 p-6">
{item.icon}
</div>
<div className="p-6">
<div className="text-4xl font-bold">{item.value}</div>
<div className="text-gray-500">{item.label}</div>
</div>
</div>
))}
</div>
</section>
{/* Team Directory Section */}
<section>
{/* Team directory content */}
<div className="flex items-center justify-between mt-4">
<h2 className="text-2xl font-bold">Team Directory</h2>
<button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2">
View All
</button>
</div>
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{teamDirectoryData.map((item, index) => (
<div
key={index}
className="rounded-lg border bg-card text-card-foreground shadow-sm"
data-v0-t="card"
>
<div className="flex flex-col space-y-1.5 p-6">
<img
src={item.image}
width="48"
height="48"
className="rounded-full"
/>
</div>
<div className="p-6">
<div className="font-semibold">{item.name}</div>
<div className="text-gray-500">{item.role}</div>
<div className="text-gray-500">{item.email}</div>
</div>
</div>
))}
</div>
</section>
{/* HR Resources Section */}
<section>
{/* HR resources content */}
<div className="flex items-center justify-between mt-4">
<h2 className="text-2xl font-bold">HR Resources</h2>
<button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2">
View All
</button>
</div>
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{hrResourcesData.map((item, index) => (
<div
key={index}
className="rounded-lg border bg-card text-card-foreground shadow-sm"
data-v0-t="card"
>
<div className="flex flex-col space-y-1.5 p-6">
{item.icon}
</div>
<div className="p-6">
<div className="font-semibold">{item.title}</div>
<div className="text-gray-500">{item.description}</div>
</div>
<div className="flex items-center p-6">
<a className="text-blue-500 hover:underline" href="#">
Read More
</a>
</div>
</div>
))}
</div>
</section>
</main>
</div>
</div>
</>
);
};
export default Page;
FACEIO provides a unique face ID for every user that is authenticated and registers on the Signup page (we will create this page in the steps below)
When a user tries to log in, the useEffect
hook in the code above is used to check if the faceId
of that user is stored in localStorage
and ensure that only authenticated users (with a stored faceId) can access the Dashboard.
If not, the user will be redirected to the login page with router.push("/");
.
You can access the JSON data here, which is used to populate the Dashboard with employee information. Add it into the Dashboard component code above after the
useEffect hook
, there is a// Add JSON DATA here
comment for clarity. π
Step 5: Creating the Signup Page
Now, we will create the Signup page where users either register or log in and will be authenticated using FACEIO before accessing the employee dashboard page.
Navigate to the Component
directory we created in Step 3, and create a SignupForm.tsx
file. Then enter the code below in SignupForm.tsx
:
"use client";
import React, { useEffect, useState } from "react";
import faceIO from "@faceio/fiojs";
import { useRouter } from "next/navigation";
import Navbar from "./Navbar";
import Sidebar from "./Sidebar";
const SignUpForm: React.FC = () => {
const router = useRouter();
const [payload, setPayload] = useState<Payload>({ userEmail: "", pin: "" });
const [isSigningUp, setIsSigningUp] = useState<boolean>(false); // New state variable
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPayload({ ...payload, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
};
const toggleSignUp = () => {
setIsSigningUp((prevState) => !prevState);
};
return (
<>
<Navbar />
<div className="flex min-h-screen flex-col">
<div className="flex flex-1">
<div className="hidden w-64 bg-gray-100 p-4 md:block">
<Sidebar />
</div>
<main className="flex-1 bg-gray-50 p-4 md:p-6">
<div className="grid gap-6">
<section className="flex items-center justify-center">
<div
className="rounded-lg border bg-card text-card-foreground shadow-sm w-full max-w-md"
data-v0-t="card"
>
<form onSubmit={handleSubmit}>
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="text-left whitespace-nowrap font-black tracking-tight text-2xl text-black">
{isSigningUp ? "Sign Up" : "Login"}
</h3>
<p className="text-sm text-muted-foreground text-gray-600 text-left">
{isSigningUp
? "Create your account by entering your email and password."
: "Enter your email and password to access your account."}
</p>
</div>
<div className="p-6 space-y-4 text-left">
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-black text-left">
Email
</label>
<input
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-black"
id="email"
placeholder="m@example.com"
type="email"
name="userEmail"
value={payload.userEmail}
onChange={onChange}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-black text-left">
Password
</label>
<input
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-black"
id="password"
type="password"
name="pin"
value={payload.pin}
onChange={onChange}
/>
</div>
</div>
<div className="flex items-center p-6">
<button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full bg-gray-900">
{isSigningUp ? "Sign Up" : "Login"}
</button>
</div>
</form>
</div>
</section>
<div className="flex justify-center">
<button
className="text-blue-600 underline mt-4"
onClick={toggleSignUp}
>
{isSigningUp ? "Login instead" : "Sign Up instead"}
</button>
</div>
</div>
</main>
</div>
</div>
</>
);
};
export default SignUpForm;
In the code above; import faceIO from "@faceio/fiojs";:
is used to import the faceIO module from the @faceio/fiojs library
, which provides the functionality for facial recognition and authentication using FACEIO. It allows us to methods like enrollUser
to register new users' facial data with FACEIO or authenticateUser
to verify a user's identity through facial recognition.
useState
hook manages the Signup component's state, while the payload
state holds the user's email (userEmail
) and PIN (pin
) for signup or login. Initially, these values are set to empty strings (""
).
The toggleSignUp
function allows us to switch between signup and login modes within the form. It toggles the isSigningUp
state from false
to true
or vice versa. When the page is loaded it is set by default to false
, representing the login mode.
Navigate to the src\app
directory where you'll find the page.tsx
file that is created by default when setting up a Next.js project. Open the file and enter the following code:
import Image from "next/image";
import Head from "next/head";
import SignUpForm from "./Component/SignupForm";
export default function Home() {
return (
<div className="bg-green-300 text-center text-white h-screen">
<Head>
<title>face.IO login/signup form</title>
<meta name="description" content="a face.IO login form" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<SignUpForm />
</main>
</div>
);
}
In the code above, we import the Signup form component using: import SignUpForm from "./Component/SignupForm";
, which will be rendered the when web application is loaded on the browser.
Step 6: Integrating FACEIO to the Next.js Web App
At this point, we have created the Sidebar, Navbar, Dashboard, and Signup/Login page.
Next, we will integrate FACEIO to authenticate users when they Signup/Login, before accessing the Dashboard.
Create a FACEIO account and sign in to your FACEIO Console to get the credentials needed to add Facial authentication.
On the FACEIO dashboard, click on New Application to create a FACEIO application, and follow the steps in the application wizard.
When asked to select a Facial Recognition Engine for the application, choose PixLab Insight. The engine will be used for mapping each enrolled userβs face in real-time into a mathematical feature vector, known as a biometric hash, which is in turn stored in a sand-boxed binary index. This is used to protect the data of users when they are authenticated with FACEIO.
Your new FACEIO app will have an ID number, take note of it as we'll be using it to integrate FACEIO into the web application.
Go to the SignupForm.tsx
file and declare a variable; faceio
that will initialise the FACEIO SDK with your specific FACEIO application ID. The FACEIO app ID will be retrieved from an environment configuration env
file.
In the root directory of your Next.js project, create a new file named .env.local
and enter this to define the environment variable:
NEXT_PUBLIC_FACE_IO_ID=your_faceio_app_id_here
Replace "your_faceio_app_id_here" with your actual FACEIO application ID, and add
.env.local
to your .gitignore file
Then continue with this code in the SignupForm.tsx
file
const SignUpForm: React.FC = () => {
const router = useRouter();
// Retrieve FACEIO application ID from environment variable
const faceioID = process.env.NEXT_PUBLIC_FACE_IO_ID;
const faceio = new faceIO(faceioID);
const [payload, setPayload] = useState<Payload>({ userEmail: "", pin: "" });
const [isSigningUp, setIsSigningUp] = useState<boolean>(false); // New state variable
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPayload({ ...payload, [e.target.name]: e.target.value });
};
Then define the enrollNewUser
function responsible for enrolling new users through FACEIO's facial recognition and authentication service like so:
const enrollNewUser = async () => {
try {
const userInfo = await faceio.enroll({
locale: "auto",
payload: {
email: payload.userEmail,
pin: payload.pin,
},
});
alert(
`User Successfully Enrolled! Details:
Unique Facial ID: ${userInfo.facialId}
Enrollment Date: ${userInfo.timestamp}
Gender: ${userInfo.details.gender}
Age Approximation: ${userInfo.details.age}`
);
localStorage.setItem("faceId", userInfo.facialId);
router.push("/dashboard");
console.log(userInfo);
} catch (error) {
console.error("Enrollment failed:", error);
}
};
This function registers a new user by default, gets the user's email and user pin as payload data and asynchronously calls the inbuilt faceio.enroll()
function from FACEIO, read more about it in the documentation.
When registration is successful, you'll see an alert alert()
message box with these details generated by FACEIO:
- FaceID
- Enrollment date
- Gender
- Approximated age
It stores the user's facial ID userInfo.facialId)
in the browser's local storage, hence the localStorage.setItem()
in the code above.
router.push()
redirects the user to the dashboard page (/dashboard) after successful enrollment.
If the facial authentication is not successful, there is a catch(error)
function that gets the error and console.logs
the error for debugging.
Now let's move on to the Login functionality by creating an authenticateUser
function, still in the SignupForm.tsx
file:
const authenticateUser = async () => {
try {
const userData = await faceio.authenticate({
locale: "auto",
});
console.log("Success, user identified");
console.log("Linked facial Id: " + userData.facialId);
console.log("Payload: " + JSON.stringify(userData.payload));
localStorage.setItem("faceId", userData.facialId); // Store face ID in local storage
router.push("/dashboard");
} catch (error) {
console.error("Authentication failed:", error);
}
};
faceio.authenticate
is another built-in function from FACEIO which checks if there is an existing user with the face that is scanned and the pin/email entered.
It then console.logs
: Success, User Identified, and stores the UserID in the browser local storage like so: localStorage.setItem("faceId", userData.facialId);
and redirects to the Dashboard page using:router.push("/dashboard");
Note that the SignUp and Login page UI are both written in the SignupForm.tsx
code, however, their functionalities differ.
We use a boolean to set the state of the Signup form:
const [isSigningUp, setIsSigningUp] = useState<boolean>(false);
By default, it is set to false, but when you click on theSign Up instead
button it sets the state to true, which then displays the sign-up form, interchanging the values per state.
That's where the toggleSignUp
function comes in like so:
const toggleSignUp = () => {
setIsSigningUp((prevState) => !prevState);
};
The handleSubmit function
checks if isSigningUp
is true, if yes it calls the enrollNewUser()
function, else it calls the authenticateUser()
function like so:
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(payload);
if (isSigningUp) {
enrollNewUser();
} else {
authenticateUser();
}
};
Step 7: Running the application
Start the development server by running:
npm run dev
The web app can be accessed in your browser with this URL
When you open the URL in your browser, the Signup/Login page loads like so:
As a new user, click on Sign Up instead, then enter your email and password to begin the authentication process with FACEIO.
Then accept the terms & conditions, allow access to your camera, and get authenticated:
You will also be prompted to set a PIN code of at least 4 digits and no more than 16 digits
When the authentication is successful, you get this displayed on your browser - FACEIO automatically generates a description of the user that has been authenticated.
Now you can Login with your email & password, complete the authentication, enter your PIN and access the employee dashboard π
And that's it! We have successfully integrated facial authentication using FACEIO into an employee dashboard web application.
To clone the project and run it locally, open your terminal and run this command:
git clone https://github.com/Tabintel/employee-next-app
Then run npm install
to install all dependencies needed for the project and npm run dev
to run the web app.
Get the full source code on GitHub.
To get started with FACEIO, sign up here and begin adding Facial authentication to your websites/web application, providing your users with more security.
Learn more about how to use FACEIO in the documentation.
Posted on May 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
May 13, 2024