Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prevent spam robot in login and register page #106

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8,768 changes: 8,768 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-google-recaptcha-v3": "^1.10.1",
"react-hook-form": "^7.52.0",
"react-icons": "^5.2.1",
"react-otp-input": "^3.1.1",
Expand Down
7 changes: 5 additions & 2 deletions src/common/components/BrandLogo/BrandLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ type BrandLogoProps = {
const BrandLogo = ({ className, isShowNikeLogo = false }: BrandLogoProps) => {
return (
<div className={`mx-10 flex justify-center ${className}`}>
<SvgIcon icon="jordan" />
<div>
<SvgIcon icon="jordan" />
</div>

{isShowNikeLogo && (
<div className="h-6/12 my-auto w-4/12 max-[600px]:hidden">
<div>
<SvgIcon icon="nike" />
</div>
)}
Expand Down
13 changes: 11 additions & 2 deletions src/common/components/SvgIcon/SvgIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@ const SvgIcon = ({ icon, ...rest }: SvgIconProps) => {
const SvgIconComponent = useMemo(() => {
switch (icon) {
case "nike":
return <img src={NikeSvgPath} alt="Nike SVG" {...rest} />;
return (
<img src={NikeSvgPath} className="w-32" alt="Nike SVG" {...rest} />
);
case "facebook":
return <img src={FbSvgPath} alt="Facebook SVG" {...rest} />;
case "jordan":
return <img src={JordanSvgPath} alt="Jordan SVG" {...rest} />;
return (
<img
src={JordanSvgPath}
className="w-32"
alt="Jordan SVG"
{...rest}
/>
);
case "nike-signup":
return <img src={NikeSignupSvgPath} alt="Nike Signup SVG" {...rest} />;
default:
Expand Down
9 changes: 6 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "@styles/index.scss";
import { BrowserRouter } from "react-router-dom";
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";

let rootElement = document.getElementById("root");

Expand All @@ -14,8 +15,10 @@ if (!rootElement) {

ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
<GoogleReCaptchaProvider reCaptchaKey={import.meta.env.VITE_SITE_KEY}>
<BrowserRouter>
<App />
</BrowserRouter>
</GoogleReCaptchaProvider>
</React.StrictMode>,
);
47 changes: 43 additions & 4 deletions src/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { isAxiosUnprocessableEntityError } from "@utils/utils.ts";

import useDocumentTitle from "@hooks/useDocumentTitle.ts";
import useDocumentTitle from "@hooks/useDocumentTitle.tsx";
import { useForm, SubmitHandler, FieldValues, Path } from "react-hook-form";
import { MouseEventHandler, useState } from "react";
import { MouseEventHandler, useCallback, useEffect, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";

import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { LoginFormData } from "@services/users.api";
import { ResponseApi } from "@utils/utils.type.ts";

Expand All @@ -24,6 +24,8 @@ import usersService from "@services/users.service";
import { useAuthStore } from "@stores/AuthStore";
import { jwtDecode } from "jwt-decode";
import { BrandLogo } from "@common/components";
import { CaptchaApiResponse } from "@services/captcha.api";
import useCaptcha from "@services/captcha.service";

type LoginFieldSchema<T extends FieldValues> = {
name: Path<T>;
Expand Down Expand Up @@ -57,6 +59,33 @@ const Login = () => {
const setAuth = useAuthStore((state) => state.setAuth);
const [errorMsg, setErrorMsg] = useState<string>("");
useDocumentTitle({ title: "Login" });
const { executeRecaptcha } = useGoogleReCaptcha();

const handleReCaptchaVerify = useCallback(async (): Promise<
CaptchaApiResponse | string
> => {
if (!executeRecaptcha) {
return "Execute recaptcha not yet available";
}

const token = await executeRecaptcha("yourAction");
try {
const response = await useCaptcha.captcha({ capcha: token });
const captcha: CaptchaApiResponse = {
success: response.data.success,
message: response.data.message,
score: response.data.score,
};

return captcha;
} catch (error) {
return "Something is wrong";
}
}, [executeRecaptcha]);

useEffect(() => {
handleReCaptchaVerify();
}, [handleReCaptchaVerify]);
const { register, handleSubmit, setError } = useForm<LoginFormData>({
resolver: yupResolver(schema),
});
Expand All @@ -67,7 +96,17 @@ const Login = () => {
},
});

const handleLogin: SubmitHandler<LoginFormData> = (data) => {
const handleLogin: SubmitHandler<LoginFormData> = async (data) => {
const captcha = await handleReCaptchaVerify();
if (typeof captcha === "string") {
toast.error(captcha);
return;
}

if (!captcha.success) {
toast.error(captcha.message);
return;
}
mutate(data, {
onSuccess: (response) => {
toast.success("Login successfully");
Expand Down
46 changes: 43 additions & 3 deletions src/pages/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as yup from "yup";

import ValidationRules from "@constants/validationRules.json";
import useWindowSize from "@hooks/useWindowSize";
import { useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { ButtonPreviewPassword, ThirdPartyButton } from "@components/index";
import useDocumentTitle from "@hooks/useDocumentTitle";
Expand All @@ -18,6 +18,10 @@ import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { FaFacebook } from "react-icons/fa";
import { FcGoogle } from "react-icons/fc";
import { SvgIcon } from "@common/components";
import { CaptchaApiResponse } from "@services/captcha.api";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import useCaptcha from "@services/captcha.service";
import { toast } from "react-toastify";

const schema: yup.ObjectSchema<Omit<RegisterForm, "email" | "phone_number">> =
yup.object().shape({
Expand Down Expand Up @@ -72,7 +76,33 @@ const Register = () => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
const toggleVisibility = () => setIsVisible(!isVisible);
const { executeRecaptcha } = useGoogleReCaptcha();

const handleReCaptchaVerify = useCallback(async (): Promise<
CaptchaApiResponse | string
> => {
if (!executeRecaptcha) {
return "Execute recaptcha not yet available";
}

const token = await executeRecaptcha("yourAction");
try {
const response = await useCaptcha.captcha({ capcha: token });
const captcha: CaptchaApiResponse = {
success: response.data.success,
message: response.data.message,
score: response.data.score,
};

return captcha;
} catch (error) {
return "Something is wrong";
}
}, [executeRecaptcha]);

useEffect(() => {
handleReCaptchaVerify();
}, [handleReCaptchaVerify]);
const { mutate, error } = useMutation({
mutationFn: (_body: Omit<RegisterForm, "agreeToTerms">) => {
return usersService.register(_body);
Expand All @@ -99,7 +129,17 @@ const Register = () => {
resolver: yupResolver(schema),
criteriaMode: "all",
});
const onSubmit: SubmitHandler<RegisterForm> = (_data) => {
const onSubmit: SubmitHandler<RegisterForm> = async (_data) => {
const captcha = await handleReCaptchaVerify();
if (typeof captcha === "string") {
toast.error(captcha);
return;
}

if (!captcha.success) {
toast.error(captcha.message);
return;
}
mutate(_data, {
onSuccess: () => {
// alert("Register successfully");
Expand Down Expand Up @@ -150,7 +190,7 @@ const Register = () => {
{width >= 1024 && (
<div className="flex flex-col items-center justify-center lg:p-[60px]">
<div>
<SvgIcon icon="nike" />
<SvgIcon className="w-96" icon="nike" />
</div>
</div>
)}
Expand Down
16 changes: 16 additions & 0 deletions src/services/captcha.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import http from "@utils/http";

export interface CapchaApi {
capcha: string;
}
export type CaptchaApiResponse = {
success: boolean;
score: number;
message: string;
};
export const callCaptcha = (_data: CapchaApi) =>
http<CaptchaApiResponse, typeof _data>({
method: "post",
url: "user/login",
data: _data,
});
7 changes: 7 additions & 0 deletions src/services/captcha.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CapchaApi, callCaptcha } from "./captcha.api";

class CaptchaService {
captcha = (data: CapchaApi) => callCaptcha(data);
}
const useCaptcha = new CaptchaService();
export default useCaptcha;