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

feat: Implemented reCAPTCHA v3 #226

Open
wants to merge 3 commits 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
20 changes: 19 additions & 1 deletion backend/routes/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import Annotated
from datetime import timedelta, datetime,date
from enum import Enum
from utils import verify_recaptcha


class CityChoice(str,Enum):
Expand Down Expand Up @@ -49,6 +50,7 @@ class CreatePatientRequest(BaseModel):
patient_phone_number:str
patient_address:str
patient_city:CityChoice = Field(default=CityChoice.DELHI)
recaptcha_token: str


class Token(BaseModel):
Expand Down Expand Up @@ -105,6 +107,12 @@ async def get_current_patient(token: Annotated[str, Depends(oauth2_bearer)]):
@auth_router.post("/signup", status_code=status.HTTP_201_CREATED)
async def create_user(db: db_dependency,
create_patient_request: CreatePatientRequest):

is_human = await verify_recaptcha(create_patient_request.recaptcha_token)
if not is_human:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail='Recaptcha verification failed.')

patient_id = str(uuid4())
current_time = datetime.utcnow()
create_user_model = Patient(
Expand All @@ -124,10 +132,20 @@ async def create_user(db: db_dependency,
db.commit()


# Extending OAuth2PasswordRequestForm to include recaptcha_token
class OAuth2PasswordRequestFormRecaptcha(OAuth2PasswordRequestForm):
recaptcha_token: str = Form(...)

# Route to create access token for a patient
@auth_router.post("/login", response_model=Token)
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestFormRecaptcha, Depends()],
db: db_dependency):

recaptcha_token = form_data.recaptcha_token
if not recaptcha_token or not await verify_recaptcha(recaptcha_token):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail='Recaptcha verification failed.')

user = authenticate_patient(form_data.username, form_data.password, db)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
16 changes: 16 additions & 0 deletions backend/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# utils.py
import httpx

async def verify_recaptcha(token: str) -> bool:
url = "https://www.google.com/recaptcha/api/siteverify"
payload = {
'secret': "6LfsY-8pAAAAANjrT-Y7an1-Va3NkUTgDp9Y_oHS",
'response': token
}
async with httpx.AsyncClient() as client:
response = await client.post(url, data=payload)
result = response.json()

score = result.get("score", 0)
threshold = 0.5
return score >= threshold and result.get("success", False)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@studio-freight/lenis": "^1.0.42",
"antd": "^5.17.3",
"axios": "^1.6.8",
"axios": "^1.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
Expand Down
71 changes: 48 additions & 23 deletions src/Pages/LoginPage.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,62 @@
import React, { useState } from "react";
import { IoEye, IoEyeOff } from "react-icons/io5";
import { Link } from "react-router-dom";
import { Helmet } from "react-helmet";
import axios from "axios";

function LoginPage() {
const [passVis, setPassVis] = useState(false);

const onSubmit = async (e) => {
e.preventDefault();

const recaptcha_token = await window.grecaptcha.execute('6LfsY-8pAAAAAKOXCgEf_anUnVPrcOhW9pfIAVyX', { action: 'submit' });

const formData=new FormData(e.target);
formData.append('recaptcha_token', recaptcha_token);

const res=await axios.post('/auth/login', formData);
}
return (
<>
<section class="bg-zinc-50 py-8 min-h-screen login">
<div class="flex flex-col items-center max-w-[28rem] justify-center px-6 py-8 mx-auto mt-7 lg:py-8">
<Helmet>
<script src="https://www.google.com/recaptcha/api.js?render=6LfsY-8pAAAAAKOXCgEf_anUnVPrcOhW9pfIAVyX" async defer></script>
</Helmet>
<section className="bg-zinc-50 py-8 min-h-screen login">
<div className="flex flex-col items-center max-w-[28rem] justify-center px-6 py-8 mx-auto mt-7 lg:py-8">
<a
href="#"
class="flex items-center mb-6 text-2xl font-semibold text-gray-900"
className="flex items-center mb-6 text-2xl font-semibold text-gray-900"
>
<img class="w-8 h-8 mr-2" src="\assets\icon.webp" alt="logo" />
<img className="w-8 h-8 mr-2" src="\assets\icon.webp" alt="logo" />
<div className="login">Nacto Care</div>
</a>
<div class="w-full bg-white rounded-lg shadow">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
<div className="w-full bg-white rounded-lg shadow">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
Sign in to your account
</h1>
<form class="space-y-4 md:space-y-6" action="#">
<form className="space-y-4 md:space-y-6" id="login-form" action="#" onSubmit={onSubmit}>
<div>
<label
for="email"
class="block mb-2 text-sm font-medium text-gray-900"
className="block mb-2 text-sm font-medium text-gray-900"
>
Your email
</label>
<input
type="email"
name="email"
id="email"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 "
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 "
placeholder="[email protected]"
required=""
/>
</div>
<div className="relative">
<label
for="password"
class="block mb-2 text-sm font-medium text-gray-900"
className="block mb-2 text-sm font-medium text-gray-900"
>
Password
</label>
Expand All @@ -49,46 +65,55 @@ function LoginPage() {
name="password"
id="password"
placeholder="••••••••"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 "
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 "
required=""
/>
<span onClick={() => setPassVis(!passVis)} className="bg-red absolute right-4 top-10 text-gray-400 hover:cursor-pointer">{passVis ? <IoEyeOff className="h-5 w-5" /> : <IoEye className="h-5 w-5" />}</span>
<span
onClick={() => setPassVis(!passVis)}
className="bg-red absolute right-4 top-10 text-gray-400 hover:cursor-pointer"
>
{passVis ? (
<IoEyeOff className="h-5 w-5" />
) : (
<IoEye className="h-5 w-5" />
)}
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-start">
<div class="flex items-center h-5">
<div className="flex items-center justify-between">
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="remember"
aria-describedby="remember"
type="checkbox"
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 "
className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 "
required=""
/>
</div>
<div class="ml-3 text-sm">
<label for="remember" class="text-gray-500">
<div className="ml-3 text-sm">
<label for="remember" className="text-gray-500">
Remember me
</label>
</div>
</div>
<a
href="#"
class="text-sm font-medium text-primary-600 hover:underline dark:text-blue-950"
className="text-sm font-medium text-primary-600 hover:underline dark:text-blue-950"
>
Forgot password?
</a>
</div>
<button
type="submit"
class="w-full text-white bg-green-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center "
className="w-full text-white bg-green-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
>
Sign in
</button>
<p class="text-sm font-light text-gray-500 dark:text-gray-400">
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
Don’t have an account yet?{" "}
<Link
to="/signup"
class="font-medium text-primary-600 hover:underline dark:text-primary-500"
className="font-medium text-primary-600 hover:underline dark:text-primary-500"
>
Sign up
</Link>
Expand Down
84 changes: 59 additions & 25 deletions src/Pages/SignupPage.jsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,63 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { IoEye, IoEyeOff } from "react-icons/io5";
import { Helmet } from "react-helmet";
import axios from "axios";

function SignupPage() {
const [passVis, setPassVis] = useState(false);
const [conVis, setConVis] = useState(false);

const onSubmit = async (e) => {
e.preventDefault();

const recaptcha_token = await window.grecaptcha.execute('6LfsY-8pAAAAAKOXCgEf_anUnVPrcOhW9pfIAVyX', { action: 'submit' });

const formData=new FormData(e.target);
formData.append('recaptcha_token', recaptcha_token);

const res=await axios.post('/auth/signup', formData);
}
return (
<>
<section class="bg-zinc-50 py-8 min-h-screen signup">
<div class="flex flex-col items-center max-w-[28rem] justify-center px-6 py-8 mx-auto mt-7 lg:py-8">
<Helmet>
<script src="https://www.google.com/recaptcha/api.js?render=6LfsY-8pAAAAAKOXCgEf_anUnVPrcOhW9pfIAVyX" async defer></script>
</Helmet>
<section className="bg-zinc-50 py-8 min-h-screen signup">
<div className="flex flex-col items-center max-w-[28rem] justify-center px-6 py-8 mx-auto mt-7 lg:py-8">
<a
href="#"
class="flex items-center mb-6 text-2xl font-semibold text-gray-900"
className="flex items-center mb-6 text-2xl font-semibold text-gray-900"
>
<img class="w-8 h-8 mr-2" src="/assets/icon.webp" alt="logo" />
<img className="w-8 h-8 mr-2" src="/assets/icon.webp" alt="logo" />
<div className="signup">Nacto Care</div>
</a>
<div class="w-full bg-white rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
<div className="w-full bg-white rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
Create an account
</h1>
<form class="space-y-4 md:space-y-6" action="#">
<form className="space-y-4 md:space-y-6" action="#" onSubmit={onSubmit}>
<div>
<label
for="email"
class="block mb-2 text-sm font-medium text-gray-900"
className="block mb-2 text-sm font-medium text-gray-900"
>
Your email
</label>
<input
type="email"
name="email"
id="email"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
placeholder="[email protected]"
required=""
/>
</div>
<div className="relative">
<label
for="password"
class="block mb-2 text-sm font-medium text-gray-900"
className="block mb-2 text-sm font-medium text-gray-900"
>
Password
</label>
Expand All @@ -50,15 +66,24 @@ function SignupPage() {
name="password"
id="password"
placeholder="••••••••"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required=""
/>
<span onClick={() => setPassVis(!passVis)} className="bg-red absolute right-4 top-10 text-gray-400 hover:cursor-pointer">{passVis ? <IoEyeOff className="h-5 w-5" /> : <IoEye className="h-5 w-5" />}</span>
<span
onClick={() => setPassVis(!passVis)}
className="bg-red absolute right-4 top-10 text-gray-400 hover:cursor-pointer"
>
{passVis ? (
<IoEyeOff className="h-5 w-5" />
) : (
<IoEye className="h-5 w-5" />
)}
</span>
</div>
<div className="relative">
<label
for="confirm-password"
class="block mb-2 text-sm font-medium text-gray-900"
className="block mb-2 text-sm font-medium text-gray-900"
>
Confirm password
</label>
Expand All @@ -67,26 +92,35 @@ function SignupPage() {
name="confirm-password"
id="confirm-password"
placeholder="••••••••"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required=""
/>
<span onClick={() => setConVis(!conVis)} className="bg-red absolute right-4 top-10 text-gray-400 hover:cursor-pointer">{conVis ? <IoEyeOff className="h-5 w-5" /> : <IoEye className="h-5 w-5" />}</span>
<span
onClick={() => setConVis(!conVis)}
className="bg-red absolute right-4 top-10 text-gray-400 hover:cursor-pointer"
>
{conVis ? (
<IoEyeOff className="h-5 w-5" />
) : (
<IoEye className="h-5 w-5" />
)}
</span>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="terms"
aria-describedby="terms"
type="checkbox"
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300"
className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300"
required=""
/>
</div>
<div class="ml-3 text-sm">
<label for="terms" class="font-light text-gray-500">
<div className="ml-3 text-sm">
<label for="terms" className="font-light text-gray-500">
I accept the{" "}
<a
class="font-medium text-primary-600 hover:underline"
className="font-medium text-primary-600 hover:underline"
href="#"
>
Terms and Conditions
Expand All @@ -96,15 +130,15 @@ function SignupPage() {
</div>
<button
type="submit"
class="w-full text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
className="w-full text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
>
Create an account
</button>
<p class="text-sm font-light text-gray-500 dark:text-gray-500">
<p className="text-sm font-light text-gray-500 dark:text-gray-500">
Already have an account?{" "}
<Link
to="/login"
class="font-medium text-primary-600 hover:underline dark:text-primary-500"
className="font-medium text-primary-600 hover:underline dark:text-primary-500"
>
Login here
</Link>
Expand Down
4 changes: 4 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ body.dark .blog {
color: white;
}

.grecaptcha-badge {
bottom: 698px !important;
}

.close-animation{
overflow: hidden;
animation: close both 0.8s ;
Expand Down