feat(signup): add password strength and breach check using HIBP API
- Implemented frontend password validation for minimum strength: - Requires 8+ characters with uppercase, lowercase, digit, and special character. - Integrated haveibeenpwned (HIBP) k-anonymity API to detect breached passwords. - Display appropriate error messages for weak or pwned passwords. - Updated Message component to support "error" and "default" types with styling. - Cleaned up SignupPage form UI and removed unused refs (e.g., roleElement). - Created passwordUtils.js to isolate SHA-1 hashing and API call logic.
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const Message = ({ message }) => {
|
const Message = ({ message, type = "error" }) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|
||||||
|
const background =
|
||||||
|
type === "error"
|
||||||
|
? "bg-red-100 border border-red-400 text-red-700"
|
||||||
|
: "bg-gray-100 border border-gray-300 text-gray-800";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-auto h-auto bg-gray-200 rounded-md text-start p-3 mx-4">
|
<div className={`rounded-md p-3 ${background}`}>
|
||||||
<p className="">{message}</p>
|
<p className="font-medium">{message}</p>
|
||||||
<p className="text-end text-sm ">
|
<p className="text-end text-sm text-gray-600">
|
||||||
{date.getDate()}/{date.getMonth()}/{date.getFullYear()}{" "}
|
{date.getDate()}/{date.getMonth() + 1}/{date.getFullYear()}{" "}
|
||||||
{date.toLocaleTimeString()}
|
{date.toLocaleTimeString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { BACKEND_URL } from "../../constants";
|
import { BACKEND_URL } from "../../constants";
|
||||||
import { t } from "../../service/translation";
|
import { t } from "../../service/translation";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
import { isPasswordPwned } from "../../utils/passwordUtils";
|
||||||
|
|
||||||
const SignupPage = (props) => {
|
const SignupPage = (props) => {
|
||||||
const outletContext = useOutletContext?.();
|
const outletContext = useOutletContext?.();
|
||||||
@@ -12,24 +13,48 @@ const SignupPage = (props) => {
|
|||||||
const firstNameElement = useRef();
|
const firstNameElement = useRef();
|
||||||
const lastNameElement = useRef();
|
const lastNameElement = useRef();
|
||||||
const emailElement = useRef();
|
const emailElement = useRef();
|
||||||
const roleElement = useRef();
|
|
||||||
const passwordElement = useRef();
|
const passwordElement = useRef();
|
||||||
|
|
||||||
|
const [error, setError] = useState("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleRegisteration = async (event) => {
|
const handleRegisteration = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
const password = passwordElement.current.value;
|
||||||
|
|
||||||
|
const strongPasswordRegex =
|
||||||
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/;
|
||||||
|
|
||||||
|
if (!strongPasswordRegex.test(password)) {
|
||||||
|
setError(
|
||||||
|
"Password must be at least 8 characters long and include uppercase, lowercase, number, and special character."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pwned = await isPasswordPwned(password);
|
||||||
|
if (pwned) {
|
||||||
|
setError("This password previously appeared in a data breach. Please use a new password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Password breach check failed:", err);
|
||||||
|
setError("Something went wrong while checking password security. Try again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
name:
|
name:
|
||||||
firstNameElement.current.value + " " + lastNameElement.current.value,
|
firstNameElement.current.value + " " + lastNameElement.current.value,
|
||||||
email: emailElement.current.value,
|
email: emailElement.current.value,
|
||||||
password: passwordElement.current.value,
|
password: password,
|
||||||
};
|
};
|
||||||
|
|
||||||
event.preventDefault();
|
try {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/v1/register`, {
|
||||||
const responce = await fetch(`${BACKEND_URL}/api/v1/register`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -37,16 +62,23 @@ const SignupPage = (props) => {
|
|||||||
body: JSON.stringify(user),
|
body: JSON.stringify(user),
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
const data = await responce.json();
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success === true) {
|
||||||
|
navigate("/user/login");
|
||||||
|
} else {
|
||||||
|
setError(data.message || "Registration failed. Please try again.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Registration error:", err);
|
||||||
|
setError("Something went wrong on the server. Please try again later.");
|
||||||
|
}
|
||||||
|
|
||||||
firstNameElement.current.value = "";
|
firstNameElement.current.value = "";
|
||||||
lastNameElement.current.value = "";
|
lastNameElement.current.value = "";
|
||||||
emailElement.current.value = "";
|
emailElement.current.value = "";
|
||||||
passwordElement.current.value = "";
|
passwordElement.current.value = "";
|
||||||
|
|
||||||
if (data.success == true) {
|
|
||||||
navigate("/user/login");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,39 +89,31 @@ const SignupPage = (props) => {
|
|||||||
{t("signup_register_heading", language)}
|
{t("signup_register_heading", language)}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-100">{t("signup_welcome", language)}</p>
|
<p className="text-gray-100">{t("signup_welcome", language)}</p>
|
||||||
<p className="text-gray-100 mb-6">
|
<p className="text-gray-100 mb-6">{t("signup_subtitle", language)}</p>
|
||||||
{t("signup_subtitle", language)}
|
<form className="space-y-6" onSubmit={handleRegisteration}>
|
||||||
</p>
|
|
||||||
<form action="#" className="space-y-6" onSubmit={handleRegisteration}>
|
|
||||||
<div className="flex flex-col gap-5 sm:flex-row">
|
<div className="flex flex-col gap-5 sm:flex-row">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label
|
<label htmlFor="firstName" className="block mb-2 text-sm font-medium text-gray-100">
|
||||||
htmlFor="firstName"
|
|
||||||
className="block mb-2 text-sm font-medium text-gray-100 dark:text-white"
|
|
||||||
>
|
|
||||||
{t("signup_first_name_label", language)}
|
{t("signup_first_name_label", language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="firstName"
|
id="firstName"
|
||||||
ref={firstNameElement}
|
ref={firstNameElement}
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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"
|
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder={t("signup_first_name_placeholder", language)}
|
placeholder={t("signup_first_name_placeholder", language)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label
|
<label htmlFor="lastName" className="block mb-2 text-sm font-medium text-gray-100">
|
||||||
htmlFor="LastName"
|
|
||||||
className="block mb-2 text-sm font-medium text-gray-100 dark:text-white"
|
|
||||||
>
|
|
||||||
{t("signup_last_name_label", language)}
|
{t("signup_last_name_label", language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="LastName"
|
id="lastName"
|
||||||
ref={lastNameElement}
|
ref={lastNameElement}
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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"
|
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder={t("signup_last_name_placeholder", language)}
|
placeholder={t("signup_last_name_placeholder", language)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -97,61 +121,44 @@ const SignupPage = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-100">
|
||||||
htmlFor="email"
|
|
||||||
className="block mb-2 text-sm font-medium text-gray-100 dark:text-white"
|
|
||||||
>
|
|
||||||
{t("signup_email_label", language)}
|
{t("signup_email_label", language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
ref={emailElement}
|
ref={emailElement}
|
||||||
className="bg-gray-50 border border-gray-300 text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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"
|
className="bg-gray-50 border border-gray-300 text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder={t("signup_email_placeholder", language)}
|
placeholder={t("signup_email_placeholder", language)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-100">
|
||||||
htmlFor="password"
|
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
{t("signup_password_label", language)}
|
{t("signup_password_label", language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
ref={passwordElement}
|
ref={passwordElement}
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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"
|
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder={t("signup_password_placeholder", language)}
|
placeholder={t("signup_password_placeholder", language)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
{error && <p className="text-red-500 text-sm font-medium">{error}</p>}
|
||||||
<input
|
|
||||||
id="remember_me"
|
|
||||||
type="checkbox"
|
|
||||||
value=""
|
|
||||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="remember_me"
|
|
||||||
className="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
{t("signup_remember_me", language)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-white w-1/2 backdrop-blur-lg bg-gradient-to-tr from-slate-100/15 to-slate-200/15 shadow-lg hover:backdrop-blur-lg focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
className="text-white w-1/2 backdrop-blur-lg bg-gradient-to-tr from-slate-100/15 to-slate-200/15 shadow-lg hover:backdrop-blur-lg focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5"
|
||||||
>
|
>
|
||||||
{t("signup_register_button", language)}
|
{t("signup_register_button", language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-600 text-center mt-4">
|
<p className="text-gray-600 text-center mt-4">
|
||||||
{t("signup_already_have_account", language)}{" "}
|
{t("signup_already_have_account", language)}{" "}
|
||||||
<Link to={"/user/login"} className="text-blue-600 hover:underline">
|
<Link to={"/user/login"} className="text-blue-600 hover:underline">
|
||||||
@@ -162,7 +169,7 @@ const SignupPage = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg shadow-md text-center w-auto">
|
<div className="rounded-lg shadow-md text-center w-auto">
|
||||||
<div className="flex flex-col items-center justify-center h-full ">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
<h1 className="text-6xl font-bold text-white mb-4 md:text-6xl lg:text-9xl ml-8">
|
<h1 className="text-6xl font-bold text-white mb-4 md:text-6xl lg:text-9xl ml-8">
|
||||||
{t("signup_journey_heading", language)}
|
{t("signup_journey_heading", language)}
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export async function isPasswordPwned(password) {
|
||||||
|
const sha1 = await hashPassword(password);
|
||||||
|
const prefix = sha1.substring(0, 5);
|
||||||
|
const suffix = sha1.substring(5);
|
||||||
|
|
||||||
|
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
const found = text
|
||||||
|
.split("\n")
|
||||||
|
.some((line) => line.split(":")[0] === suffix.toUpperCase());
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hashPassword(password) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(password);
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user