When applying for a tech job it is important to have a strong portfolio to impress the company. Especially for beginner developers the first impression of resume and projects are playing crucial role. The most common project types are To-do app, Weather app, Blog and etc. which is boring.
In this new series we will build unique and creative stuff and ship it to production. So, in this episode we’re going to build a simple app that will check phishing risk against provided URL.
We’re going to use a NextJS to build our app fast and ship it to production easily using Vercel. Each time we make a new commit Vercel will update our app so we don’t need to worry about deployment but only focus on building.
Why this project is great?
Because it involves many skills those required to have as a developer:
- TypeScript.
- Third-party integration.
- Use of NextJS itself.
- UI/UX skills.
- Tailwind CSS (eh)
- Deployment
What we are building?
This project is going to be a single page app that will let user to enter a URL. After user enters the URL, it should extract the domain and send it to third-party service which will check the phishing risk of that domain. Then, on the backend we need to evaluate the response from that service and display the result to the user with nice UI/UX interface.
Getting Started
Let’s start by creating a new NextJS project. I will be using latest version of NextJS which is version 14 at time of writing this post.
npx create-next-app@latest
It will prompt few questions to configure the NextJS project. I will select to use TypeScript, initialise the Tailwind CSS and use App router.
✔ What is your project named? … <your_app_name_here>
✔ Would you like to use TypeScript? … No / <u>**Yes**</u>
✔ Would you like to use ESLint? … No / <u>**Yes**</u>
✔ Would you like to use Tailwind CSS? … No / <u>**Yes**</u>
✔ Would you like to use `src/` directory? … <u>**No**</u> / Yes
✔ Would you like to use App Router? (recommended) … No / <u>**Yes**</u>
✔ Would you like to customize the default import alias (@/*)? … No / <u>**Yes**</u>
Now you should have initialised NextJS project ready to use.
Integration with VirusTotal
Virus Total is an online service that analyses suspicious files and URLs to detect types of malware and malicious content using antivirus engines and website scanners.
Create an account on VirusTotal to get an API key for interacting with their service programmatically.
Since we want to check domain safety we had to use Domain Report endpoint to gather deep analyse of the submitted domain.
We can test the endpoint with Postman to see which part of the response we need to calculate the risk of phishing. Here’s the results:
There are lots of useful information on domain analysis based on various needs. But for now, we will continue with selected part which will help us to calculate the risk.
Building Backend
Since we have all information about what we’re going to build, it’s time to put these knowledge into practice. First, create a new API route that will send request to VirusTotal:
app/api/virustotal/route.ts
import { calcRisk } from '@/utils/calcRiskcomponents'
import axios from 'axios'
import { type NextRequest } from 'next/server'
import { getDomain } from 'tldts'
export async function GET(request: NextRequest) {
const urlToAnalyze = request.nextUrl.searchParams.get('urlToAnalyze')
if (!urlToAnalyze) {
return new Response("URL must not be empty.", {status: 400})
}
try {
const domain = getDomain(urlToAnalyze)
const virustotalRes = await axios.get(`https://www.virustotal.com/api/v3/domains/${domain}`, {
headers: {
"x-apikey": process.env.VIRUSTOTAL_API_KEY
}
})
const stats = calcRisk(virustotalRes?.data?.data?.attributes?.last_analysis_stats)
return Response.json({ stats: stats })
} catch (err) {
return new Response("Internal Server Error", {status: 500})
}
}
This route will handle the GET request that will be sent from frontend part of our app. Let’s break down the code:
- Validating if required search param named
urlToAnalyze
is present. This param should be URL that provided by user. - If
urlToAnalyze
is present then we will extract the domain from that URL using npm module namedtldts
. - Then, based on VirusTotal docs we’re sending the domain to gather analysis report. Also we’re including the API Key in the headers.
- We have a helper util function
calcRisk
which will evaluate the result oflast_analysis_stats
part from the response.
So, the calcRisk
is a simple helper function that will determine if URL is risky or not. You can change the logic as you want such as assigning weights to the states of analysis. I would mark the domain “High Risk” even if there are only single report from antivirus engines or scanners.
So here’s the code for better understanding:
utils/calcRisk.ts
interface VirustotalStatsForDomain {
harmless: number,
malicious: number,
suspicious: number,
undetected: number,
timeout: number
}
export function calcRisk(virsusTotalStatsForDomain: VirustotalStatsForDomain) {
const { malicious, suspicious } = virsusTotalStatsForDomain;
if(malicious > 0) {
return "High Risk"
}
if (suspicious > 0) {
return "Moderate Risk"
}
return "Low Risk"
}
Cool, that’s all for backend. Now, let’s move on to frontend part.
Building Frontend
We’re using app router in our NextJS project, so we should create the pages as <folder-name>/page.tsx
which in this case I will create it as check-site/page.tsx
. That will be routable on browser as /check-site
.
If I would be brutally honest, Tailwind CSS is something that I don’t prefer but it’s useful for small projects like this.
Well, here’s the code for frontend:
/app/check-site/page.tsx
"use client"
import Hero from "@/components/Herocomponents"
import axios from "axios"
import { useState } from "react"
export default function Page() {
const [url, setUrl] = useState<string>("");
const [analyzeResult, setAnalyzeResult] = useState<string | null>(null)
const handleUrlAnalyze = async () => {
const res = await axios.get(`/api/virustotal?urlToAnalyze=${url}`)
setAnalyzeResult(res?.data?.stat)
}
const icon: any = {
"Low Risk": <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-shield-check"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="m9 12 2 2 4-4"/></svg>,
"Moderate Risk": <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 24 24" fill="none" stroke="yellow" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-shield-alert"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>,
"High Risk": <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-shield-x"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="m14.5 9-5 5"/><path d="m9.5 9 5 5"/></svg>,
}
return (
<section className="flex flex-col justify-between bg-white dark:bg-gray-900 min-h-screen">
<div>
<Hero />
<div className="px-4 mx-auto max-w-2xl flex flex-col items-center justify-center">
<div className="w-full flex flex-col items-center">
<div className="sm:col-span-2 w-full">
<label className="block mb-2 text-md font-medium text-gray-900 text-center dark:text-white">Domain or URL to Analyze</label>
<input type="text" name="name" id="name" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" placeholder="Type domain to check"
onChange={(e) => setUrl(e.target.value)}
/>
</div>
</div>
<button type="button" className="inline-flex items-center px-5 py-2.5 mt-4 sm:mt-6 text-sm font-medium text-center text-white bg-blue-900 rounded-lg hover:bg-primary-800"
onClick={handleUrlAnalyze}
>
Check URL
</button>
</div>
<div className="flex flex-col justify-center items-center w-full py-5">
{analyzeResult && icon[analyzeResult]}
<h3 className="text-md">{analyzeResult}</h3>
</div>
</div>
{/* Image container positioned at the bottom */}
<div className="flex justify-center items-center w-full py-5">
Made with
<img className="ml-1" src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/VirusTotal_logo.svg/2560px-VirusTotal_logo.svg.png" width={100} alt="VirusTotal logo" />
</div>
</section>
)
}
The only hidden part in the code is <Hero />
component which is below:
components/Hero.tsx
const Hero = () => {
return (
<section className="bg-white dark:bg-gray-900">
<div className="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 lg:px-12">
<h1 className="mb-4 text-4xl font-extrabold tracking-tight leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">Secure Your Surfing</h1>
<p className="mb-8 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 xl:px-48 dark:text-gray-400">Navigate the web with confidence using instant site safety checker.</p>
<div className="flex justify-center">
<img className="h-auto max-w-full" src="logo.png" width={200} alt="image description" />
</div>
</div>
</section>
)
}
export default Hero;
And yes, I generated the tailwind code with ChatGPT.
Result
Now, we have full working, single feature app that will analyse the phishing risk of submitted URL. Here’s how it looks when you run the app:
What’s next?
You need to deploy your NextJS to Vercel to make it publicly accessible for others. Also, remember to explain your solution well in ReadMe file since it shows that you’re documenting your work.
PylotStuff / phishalayzer
Single feature app to check phishing risk of URLs
This is a Next.js project bootstrapped with create-next-app
.
Getting Started
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
Open http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx
. The page auto-updates as you edit the file.
This project uses next/font
to automatically optimize and load Inter, a custom Google Font.
Learn More
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
Deploy on Vercel
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.
Cross-Published with OnePublish.
Top comments (0)