In this tutorial, we are building an online voting web app with Next.js and FACEIO, where users can register, verify their age, authenticate their identity via facial recognition, and cast their votes on community projects.
This project will use FACEIO's SDK to ensure the authenticity and security of each vote.
At the end of this tutorial, this is what the voting page will look like, which can only be accessed after authentication with FACEIO 😀
Ready? Let's get started!
Prerequisites
- Basic understanding of Next.js and Tailwind CSS
- A FACEIO account
- A package manager installed (NPM), and a Code editor (VS Code or your favourite)
- FACEIO/fiojs NPM package installed.
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.
Key Features of FACEIO to be implemented in the web app:
- Age Verification: Ensure voters meet the age requirement of 18 years and above.
- Facial Authentication: Authenticate voters using facial recognition.
- Anti-Spoofing & Deepfake Prevention: Prevent fraudulent voting attempts with advanced security measures.
FACEIO implements these in a single SDK so web developers can secure their web apps with FACEIO.
Let's begin!
Step 1: Developing the Client-Side: Setting Up Your App Router
Open your terminal, navigate to your workspace folder, and run the command below:
npx create-next-app@latest secure-voting
This creates a new project secure-voting
, which we use to build our application.
Select Yes for all the prompts as shown below (except the last one, as the default import alias is recommended) to install Typescript, TailwindCSS, and other dependencies needed for the Next.js project.
Navigate to the project directory like so:
cd secure-voting
And run this command to open the project in VS Code editor:
code .
To start building the client side of your secure voting web app, you'll first need to set up the application's routing. You'll create three essential pages:Home, Login, and Register. These pages will guide your users of the app through account creation, authentication, and participation in voting.
Creating the Homepage
First, create a root component to manage rendering the Homepage. This component will ensure that the web app checks whether the document environment is initialized before rendering the Homepage.
This is important to avoid errors related to undefined document objects, especially when using Next.js.
In the app\(Home)
create a page.js
file and enter the code below:
"use client";
import Home from "./_components";
import { useState, useEffect } from "react";
export default function Root() {
const [docEnv, setDocEnv] = useState(false);
useEffect(() => {
if (typeof document !== "undefined") {
setDocEnv(true);
}
}, []);
return <div className="relative">{docEnv && <Home />}</div>;
}
Let’s break down what this code does:
The
useState
anduseEffect
hooks from the React library tracks whether the document environment (docEnv
) is initialized and updates the state once the component is loaded.useEffect
also ensures thedocument
object is defined, and theHome
component only renders when the app is running in a browser environment, preventing issues with server-side rendering in Next.js.
Step 2: Building the Home Page Components
In this section, you'll implement the logic for your Home page. The goal is to ensure that users who aren't logged in are redirected to the Login page, while authenticated users can stay on the Home page. Additionally, you'll implement functionality to check voting eligibility based on user information.
Checking User Authentication
To verify if a user is logged in and has valid credentials, you must check if userInfo
is stored in the localStorage
. If userInfo
is not found, you'll redirect the user to the Login page.
In the app\(Home)\_components
directory, create a page.js
file and enter the code below:
const router = useRouter();
let userinfo;
if (typeof window !== "undefined") {
userinfo = JSON.parse(localStorage.getItem("userinfo"));
}
console.log(userinfo);
useEffect(() => {
if (!userinfo) {
router.push("/login");
}
}, [userinfo, router]);
As we are building the homepage and other components of the voting app, we have to look for the userInfo details located and stored in the localStorage
with the Key userInfo
The main idea is that if this data stored with the key userInfo
is null
, the user should be directed to the login page, and still be on the Homepage. So, we use the router.push('/login)
method from the useRouter
hook to achieve this.
Also, we must check if the window object
has been defined before setting the value from the localStorage
to the userInfo
variable. Without doing this, the whole initialization would be flagged as an error by Nextjs.
When the user has been authenticated on the login page, the next step is to create a function to check for the user's eligibility through the userInfo
object.
Here is the function:
const handleVotingEligibility = () => {
if (userinfo?.details?.age < 18) {
toast.error("You are not eligible to vote");
} else {
toast.error("You are eligible to vote");
}
};
💡 Note that the
handleVotingEligibility
function will interact with the details from the FACEIO verification of the user when signing up.
If the user's age is under 18, it displays an error message; otherwise, it confirms eligibility.
The voting application does not directly ask for the user's age; the FACEIO SDK automates this process, and the result is sent to the function, which allows the user to get registered on the platform or not.
Step 3: Developing the Login page
In the Login screen, we have to check that the id
of the document has been initialized. If the document has been initialized, we render the home components.
Create another page.js
file, but this time in the app\login\
directory, and enter the code below:
"use client";
import Login from "./_components";
import { useState, useEffect } from "react";
export default function Root() {
const [docEnv, setDocEnv] = useState(false);
useEffect(() => {
if (typeof document !== "undefined") {
setDocEnv(true);
}
}, []);
return <div className="relative">{docEnv && <Login />}</div>;
}
The
"use client";
directive ensures that the code is executed only on the client side, not during server-side rendering.useState
hook initializesdocEnv
tofalse
, which indicates whether the document environment has been initialized, while theuseEffect
hook checks ifdocument
is defined to confirm the code is running in a client environment. If so, it setsdocEnv
totrue
.The
Login
component is rendered conditionally only ifdocEnv
istrue,
ensuring it only loads after the document environment is ready.
Step 4: Building the Login component logic to integrate with FACEIO
Before we proceed with the logic component, follow this steps to setup your account on FACEIO:
- 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 use it to integrate FACEIO into the web application.
Also note the API key, as it'll be used in the application
- 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
NEXT_PUBLIC_KEY=your_faceio_api_key
Replace "your_faceio_app_id_here" with your FACEIO application ID and "your_faceio_api_key" with your FACEIO API key, then add
.env.local
to your .gitignore file
Now that's done, let's proceed
To set up the Login component, we must integrate the FACEIO SDK and handle user input efficiently.
Run this command in your terminal to install the FACEIO/fiojs NPM package installed for handling facial authentication.
npm i @faceio/fiojs
When it is installed, create an index.jsx
file in the app\login\_components
directory and initialize FACEIO.
Enter the code like so:
import faceIO from "@faceio/fiojs";
let faceio = new faceIO(process.env.NEXT_PUBLIC_KEY);
In the code above, we import the faceIO
SDK and then initialize it to a variable.
The API key is stored in the environment variables (
.env
) file underNEXT_PUBLIC_KEY
.
Next, set up the form input states to capture the user's email and PIN. These form inputs are managed by creating a state variable called payload
. The variable will hold the email
and pin
values.
We define an onChange
function that dynamically updates the payload
state based on the user’s input to handle changes in the input fields.
Enter this code in the index.jsx
file stated above
const [payload, setPayload] = useState({ email: "", pin: "" });
const onChange = (e) => {
setPayload({ ...payload, [e.target.name]: e.target.value });
};
- The
useState
hook initializespayload
with empty strings foremail
andpin
. - The
onChange
function updates thepayload
state with the values entered in the form fields, setting eitheremail
orpin
based on thename
attribute of the input element.
This configuration prepares the Login component for user authentication and input - which is the SignUp/Login process in the voting web app.
Step 5: Handling User Authentication Logic
With the onChange
function in place to handle user input, the next step is to implement user authentication. This involves creating a function that interacts with the FACEIO SDK to authenticate users based on their email and PIN.
First, define the AuthenticateUser
function. This function will send the user's credentials to FACEIO for authentication.
Still in the index.jsx
file, enter this code:
const AuthenticateUser = async () => {
try {
const userInfo = await faceio.enroll({
locale: "auto",
token: process.env.NEXT_PUBLIC_FACE_IO_ID, // we use the environment variable here
payload: {
email: payload.email,
pin: payload.pin,
},
});
// Store user info in localStorage
localStorage.setItem("userinfo", JSON.stringify(userInfo));
// Navigate to the home page upon successful authentication
if (userInfo) {
router.push("/");
}
console.log(userInfo);
} catch (error) {
console.error("Enrollment failed:", error);
}
};
In this function:
- The
faceio.enroll
method sends thepayload
containing the email and PIN to FACEIO for authentication. - Upon successful authentication, the returned
userInfo
is stored inlocalStorage
for session management. - If the authentication is successful, the user is redirected to the home page using
router.push("/")
. - If an error occurs, it is logged to the console.
Next, we need a way to trigger this authentication process when the user submits the login form. To do this, implement the handleSubmit
function:
Yes 😎, enter this code in the same index.jsx
file @app\login\_components
const handleSubmit = async (e) => {
e.preventDefault(); // Prevent default form submission behavior
// Check if both email and PIN fields are filled
if (!payload?.email || !payload?.pin) {
toast.error("Please fill in the user credentials");
} else {
// Proceed with authentication if fields are valid
AuthenticateUser();
}
};
In this function:
- The
e.preventDefault()
call prevents the default form submission, which would otherwise reload the page. - It checks if both the
email
andpin
fields are filled. If either is missing, an error message is displayed usingtoast.error
. - If both fields are provided, the
AuthenticateUser
function is called to process the authentication.
Step 6: Redirecting after the FACEIO Authentication
After a user successfully logs in, we must ensure they are redirected to the homepage if their authentication is valid.
This process involves checking if the userInfo data
, which is stored in localStorage
by FACEIO, exists. If it does, the user should be redirected to the homepage.
Enter the code below @index.jsx
file to implement this in the useEffect
hook:
useEffect(() => {
// Ensure that the window object is defined before accessing localStorage
if (typeof window !== "undefined") {
// Retrieve user information from localStorage
const userinfo = localStorage.getItem("userinfo");
// Redirect to the home page if userInfo exists
if (userinfo !== null) {
router.push("/");
}
}
}, [router]);
- The
window
object is defined to ensure the code runs only on the client side. This avoids issues during server-side rendering. -
userInfo
data is retrieved fromlocalStorage
, which was set by FACEIO during the authentication process. FACEIO’senroll
method not only performs facial recognition but also stores user information, ensuring secure access. - If the
userInfo
data is found (i.e., it is notnull
), it indicates that FACEIO has successfully authenticated the user. As a result, we userouter.push("/")
to redirect the user to the home page.
Step 7: Integrating Components and UI Elements
Next, we'll integrate components and some UI elements for the application.
In the app
directory, create a register/_components
directory and then an index.jsx
file. This will contain the main components of the login interface.
First, let’s define the structure of the Login
component. This component renders the Navbar
and MainContent
components, which together form the layout of the login page.
"use client";
import React, { useEffect, useState } from "react";
import { Camera, Lock } from "lucide-react";
import faceIO from "@faceio/fiojs";
import { useRouter } from "next/navigation";
import Link from "next/link";
import toast from "react-hot-toast";
In the code above, we import necessary modules and components. faceIO
from @faceio/fiojs
is imported to handle the facial recognition authentication.
const Login = () => {
return (
<div className="w-full">
<Navbar />
<MainContent />
{/* <Footer/> */}
</div>
);
};
The Login
component uses a layout that includes a Navbar
and MainContent
section. You can also include a footer if needed.
Navbar Component
const Navbar = () => {
const navbarlist = ["About", "Contact"];
return (
<div className="w-full py-6 bg-[#000] flex">
<div className="w-full px-8 flex items-center justify-between">
<span className="text-2xl font-bold flex items-center gap-3 text-white">
<Lock />
Secure Voting
</span>
<div className="flex items-center gap-8 justify-end">
<div className="hidden lg:flex items-center gap-8">
{navbarlist?.map((nav, index) => {
return (
<Link
key={index}
className="text-base font-normal text-[#fff]"
href={"/"}
>
{nav}
</Link>
);
})}
</div>
</div>
</div>
</div>
);
};
The Navbar
component provides navigation links like "About" and "Contact." It uses Link
from Next.js to handle routing.
MainContent Component
const MainContent = () => {
const router = useRouter();
const [payload, setPayload] = useState({ email: "", pin: "" });
const [isSigningUp, setIsSigningUp] = useState(false); // New state variable
const onChange = (e) => {
setPayload({ ...payload, [e.target.name]: e.target.value });
};
const faceio = new faceIO(process.env.NEXT_PUBLIC_KEY);
let faceInfo;
if (typeof window !== "undefined") {
faceInfo = localStorage.getItem("faceId");
}
const toggleSignUpPage = () => {
setIsSigningUp((prevState) => !prevState);
};
const registerNewUser = async () => {
try {
const userInfo = await faceio.enroll({
locale: "auto",
token: "fioaf212",
payload: {
email: payload.email,
pin: payload.pin,
},
});
localStorage.setItem("userinfo", JSON.stringify(userInfo));
if (userInfo) {
router.push("/");
}
console.log(userInfo);
} catch (error) {
console.error("Enrollment failed:", error);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!payload?.email && !payload?.pin) {
toast.error("Please fill in the user credentials");
} else {
registerNewUser();
}
};
useEffect(() => {
if (typeof window !== "undefined") {
const userinfo = localStorage.getItem("userinfo");
if (userinfo !== null) {
router.push("/");
}
}
}, [router]);
return (
<div className="w-full min-h-[100vh] items-center justify-center py-24 flex flex-col gap-4">
<div className="w-[90%] rounded-2xl md:w-[500px] mx-auto border py-12 px-8 flex flex-col gap-8 ">
<span className="text-3xl md:text-4xl font-bold gap-3 text-[#000]">
Sign Up
<span className="block font-normal text-base pt-2 text-grey">
Create your account
</span>
</span>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
{LoginFormInputData.map((form, index) => (
<label
key={index}
htmlFor={form?.label}
className="text-base flex flex-col gap-2 font-semibold"
>
{form?.label}
<input
type={form?.type}
onChange={onChange}
name={form?.name}
value={payload[form?.name]}
placeholder={form?.placeholder}
className="font-normal px-6 rounded-md w-full h-[50px] border outline-none"
/>
</label>
))}
<button
type="submit"
className="p-3 hover:opacity-[.6] cursor-pointer px-4 rounded-sm text-base text-center font-semibold text-[#fff] bg-[#000]"
>
Sign up
</button>
</form>
<div className="flex px-4">
<span className="flex text-base items-center gap-2">
Already a member?{" "}
<Link href={"/login"} className="font-bold underline">
Sign In
</Link>
</span>
</div>
</div>
</div>
);
};
In the MainContent
component:
-
faceIO
is initialized using the key from the environment variables. -
useState
manages form inputs (payload
) and a state for toggling sign-up pages (isSigningUp
). -
onChange
function updates the form state dynamically as the user types. -
registerNewUser
function handles the registration process using FACEIO'senroll
method. If successful, it stores the user information inlocalStorage
and redirects to the home page. -
handleSubmit
function prevents the default form submission behavior, validates the inputs, and callsregisterNewUser
. -
useEffect
checks ifuserInfo
is present inlocalStorage
and redirects to the home page if it is.
This setup ensures that FACEIO’s authentication is integrated into the login page, handling user input and redirection based on the authentication status.
Running the application
Run this command in your terminal to start the application:
npm run dev
Then copy this URL and open it in your browser:
http://localhost:3000
The secure voting app loads like so:
To proceed, enter your email, set a password (that you can remember), and click on Sign Up
Then, the FACEIO process begins like so:
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, and FACEIO confirms you are above 18 years, you can proceed to view the community voting page! 😎
In your FACEIO console dashboard, you can also view the recently enrolled users and their indexed facial IDs to monitor and keep track of the application.
And that's it! We have successfully integrated facial authentication using FACEIO into a voting application!
To clone the project and run it locally, open your terminal and run this command:
git clone https://github.com/Tabintel/secure-voting
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.
If you’re looking to add cutting-edge security features to your own projects, sign up on FACEIO today. It’s a powerful tool for easily integrating facial recognition into your applications.
Thank you for reading! 🍻
Resources
The FACEIO Forum for community support.
Top comments (0)