Anish commited on
Commit
86c1923
·
1 Parent(s): bf557e8

[Feature Added] > Added Cloudflare Turnstile captcha for better security and bot disengagement. Also updated features, like file_routes, auth_routes to support Turnstile.

Browse files
backend/app/api/auth_routes.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
2
  from sqlalchemy.orm import Session
3
  from app.schemas.auth_schema import LoginRequest, PasswordResetRequest, PasswordResetConfirm, VerifyEmailConfirm, ResendVerificationRequest
4
  from app.services.auth_service import login_user, request_password_reset, confirm_password_reset, confirm_email_verification, resend_verification
@@ -6,13 +6,17 @@ from app.db.session import get_db
6
  from app.auth.oauth import oauth
7
  from app.services.oauth_service import handle_oauth_login
8
  from app.services.email_service import send_reset_password_email, send_verification_email
 
9
 
10
  router = APIRouter(prefix="/auth", tags=["Auth"])
11
 
12
  @router.post("/login")
13
- def login(data: LoginRequest, request: Request, db: Session = Depends(get_db)):
14
  try:
 
 
15
  token = login_user(db, data.email, data.password, request)
 
16
  if not token:
17
  raise HTTPException(status_code=401, detail="Invalid Credentials")
18
  return {"access_token": token}
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks, Header
2
  from sqlalchemy.orm import Session
3
  from app.schemas.auth_schema import LoginRequest, PasswordResetRequest, PasswordResetConfirm, VerifyEmailConfirm, ResendVerificationRequest
4
  from app.services.auth_service import login_user, request_password_reset, confirm_password_reset, confirm_email_verification, resend_verification
 
6
  from app.auth.oauth import oauth
7
  from app.services.oauth_service import handle_oauth_login
8
  from app.services.email_service import send_reset_password_email, send_verification_email
9
+ from app.security.turnstile import verify_turnstile_token
10
 
11
  router = APIRouter(prefix="/auth", tags=["Auth"])
12
 
13
  @router.post("/login")
14
+ async def login(data: LoginRequest, request: Request, user_data: dict, cf_turnstile_response: str = Header(None, alias="cf-turnstile-response"), db: Session = Depends(get_db)):
15
  try:
16
+ client_ip = request.client.host
17
+ await verify_turnstile_token(cf_turnstile_response, client_ip)
18
  token = login_user(db, data.email, data.password, request)
19
+
20
  if not token:
21
  raise HTTPException(status_code=401, detail="Invalid Credentials")
22
  return {"access_token": token}
backend/app/api/file_routes.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, Depends, UploadFile, File as FastAPIFile, Request
2
  from sqlalchemy.orm import Session
3
  from app.db.session import get_db
4
  from app.core.auth_dependancy import get_current_user
@@ -9,6 +9,7 @@ from app.services.file_delete_service import delete_file_service
9
  from app.models.user_model import User
10
  from app.worker.tasks import process_file_task
11
  from app.core.limiter import limiter
 
12
 
13
  router = APIRouter(prefix="/files", tags=["files"])
14
 
@@ -59,12 +60,16 @@ def get_file(
59
 
60
  @router.post("/upload")
61
  @limiter.limit("5/minute")
62
- def upload_file(
63
  request: Request,
64
  file: UploadFile = FastAPIFile(...),
 
65
  db: Session = Depends(get_db),
66
  current_user: User = Depends(get_current_user)
67
  ):
 
 
 
68
  saved_file = upload_file_service(
69
  db=db,
70
  file=file,
 
1
+ from fastapi import APIRouter, Depends, UploadFile, File as FastAPIFile, Request, Header
2
  from sqlalchemy.orm import Session
3
  from app.db.session import get_db
4
  from app.core.auth_dependancy import get_current_user
 
9
  from app.models.user_model import User
10
  from app.worker.tasks import process_file_task
11
  from app.core.limiter import limiter
12
+ from app.security.turnstile import verify_turnstile_token
13
 
14
  router = APIRouter(prefix="/files", tags=["files"])
15
 
 
60
 
61
  @router.post("/upload")
62
  @limiter.limit("5/minute")
63
+ async def upload_file(
64
  request: Request,
65
  file: UploadFile = FastAPIFile(...),
66
+ cf_turnstile_response: str = Header(None, alias="cf-turnstile-response"),
67
  db: Session = Depends(get_db),
68
  current_user: User = Depends(get_current_user)
69
  ):
70
+ client_ip = request.client.host
71
+ await verify_turnstile_token(cf_turnstile_response, client_ip)
72
+
73
  saved_file = upload_file_service(
74
  db=db,
75
  file=file,
backend/app/api/user_routes.py CHANGED
@@ -1,18 +1,24 @@
1
- from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
2
  from sqlalchemy.orm import Session
3
  from app.schemas.user_schema import UserCreate
4
  from app.services.user_service import create_user
5
  from app.db.session import get_db
6
  from app.services.email_service import send_verification_email
 
7
 
8
  router = APIRouter(prefix="/users", tags=["Users"])
9
 
10
  @router.post("/register")
11
- def register(
 
12
  user: UserCreate,
13
  background_tasks: BackgroundTasks,
 
14
  db: Session = Depends(get_db)
15
  ):
 
 
 
16
  new_user, raw_token = create_user(
17
  db,
18
  email=user.email,
 
1
+ from fastapi import Header, Request, APIRouter, Depends, HTTPException, BackgroundTasks
2
  from sqlalchemy.orm import Session
3
  from app.schemas.user_schema import UserCreate
4
  from app.services.user_service import create_user
5
  from app.db.session import get_db
6
  from app.services.email_service import send_verification_email
7
+ from app.security.turnstile import verify_turnstile_token
8
 
9
  router = APIRouter(prefix="/users", tags=["Users"])
10
 
11
  @router.post("/register")
12
+ async def register(
13
+ request: Request,
14
  user: UserCreate,
15
  background_tasks: BackgroundTasks,
16
+ cf_turnstile_response: str = Header(None, alias="cf-turnstile-response"),
17
  db: Session = Depends(get_db)
18
  ):
19
+ client_ip = request.client.host
20
+ await verify_turnstile_token(cf_turnstile_response, client_ip)
21
+
22
  new_user, raw_token = create_user(
23
  db,
24
  email=user.email,
backend/app/core/config.py CHANGED
@@ -19,6 +19,7 @@ class Settings(BaseSettings):
19
  SMTP_PORT: int = 587
20
  SMTP_SERVER: str | None = None
21
  FRONTEND_URL: str = "http://localhost:3000"
 
22
 
23
  model_config = SettingsConfigDict(
24
  env_file=".env",
 
19
  SMTP_PORT: int = 587
20
  SMTP_SERVER: str | None = None
21
  FRONTEND_URL: str = "http://localhost:3000"
22
+ TURNSTILE_SECRET_KEY: str | None = None
23
 
24
  model_config = SettingsConfigDict(
25
  env_file=".env",
backend/app/security/turnstile.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import logging
3
+ from fastapi import HTTPException
4
+ from app.core.config import settings
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ async def verify_turnstile_token(token: str, ip_address: str = None) -> bool:
9
+ if not token:
10
+ logger.warning("Turnstile Verification Failed: Missing token.")
11
+ raise HTTPException(status_code=400, detail="CAPTCHA token is missing.")
12
+
13
+ verify_url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
14
+ payload = {
15
+ "secret": settings.TURNSTILE_SECRET_KEY,
16
+ "response": token
17
+ }
18
+
19
+ if ip_address:
20
+ payload["remoteip"] = ip_address
21
+
22
+ try:
23
+ async with httpx.AsyncClient(timeout=5.0) as client:
24
+ response = await client.post(verify_url, data=payload)
25
+ result = response.json()
26
+
27
+ if result.get("success"):
28
+ return True
29
+ else:
30
+ error_codes = result.get("error-codes", [])
31
+ logger.warning(f"Turnstile Verification Failed. Errors: {error_codes}")
32
+
33
+ if "timeout-or-duplicate" in error_codes:
34
+ raise HTTPException(status_code=400, detail="CAPTCHA expired or already used. Please refresh.")
35
+
36
+ raise HTTPException(status_code=403, detail="CAPTCHA verification failed.")
37
+
38
+ except httpx.RequestError as e:
39
+ logger.error(f"Turnstile API connection error: {str(e)}")
40
+ raise HTTPException(status_code=503, detail="Security validation service unavailable.")
frontend/components/ui/TurnstileWidget.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Turnstile } from '@marsidev/react-turnstile';
2
+ import { useState } from 'react';
3
+
4
+ interface TurnstileProps {
5
+ onVerify: (token: string) => void;
6
+ actionName?: string
7
+ }
8
+
9
+ export default function TurnstileWidget({ onVerify, actionName = "login" }: TurnstileProps) {
10
+ const [token, setToken] = useState<string | null>(null);
11
+
12
+ const handleSuccess = (token: string) => {
13
+ setToken(token);
14
+ onVerify(token);
15
+ };
16
+
17
+ const handleError = () => {
18
+ console.error("Turnstile encountered an error.");
19
+ setToken(null);
20
+ }
21
+
22
+ return (
23
+ <div className="my-4">
24
+ <Turnstile
25
+ siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || ""}
26
+ onSuccess={handleSuccess}
27
+ onError={handleError}
28
+ onExpire={() => setToken(null)}
29
+ options={{
30
+ action: actionName,
31
+ theme: "auto",
32
+ }}
33
+ />
34
+ </div>
35
+ )
36
+ }
frontend/package-lock.json CHANGED
@@ -8,6 +8,7 @@
8
  "name": "frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
 
11
  "class-variance-authority": "^0.7.1",
12
  "clsx": "^2.1.1",
13
  "lucide-react": "^0.575.0",
@@ -1595,6 +1596,16 @@
1595
  "@jridgewell/sourcemap-codec": "^1.4.14"
1596
  }
1597
  },
 
 
 
 
 
 
 
 
 
 
1598
  "node_modules/@modelcontextprotocol/sdk": {
1599
  "version": "1.26.0",
1600
  "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
 
8
  "name": "frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@marsidev/react-turnstile": "^1.4.2",
12
  "class-variance-authority": "^0.7.1",
13
  "clsx": "^2.1.1",
14
  "lucide-react": "^0.575.0",
 
1596
  "@jridgewell/sourcemap-codec": "^1.4.14"
1597
  }
1598
  },
1599
+ "node_modules/@marsidev/react-turnstile": {
1600
+ "version": "1.4.2",
1601
+ "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.4.2.tgz",
1602
+ "integrity": "sha512-xs1qOuyeMOz6t9BXXCXWiukC0/0+48vR08B7uwNdG05wCMnbcNgxiFmdFKDOFbM76qFYFRYlGeRfhfq1U/iZmA==",
1603
+ "license": "MIT",
1604
+ "peerDependencies": {
1605
+ "react": "^17.0.2 || ^18.0.0 || ^19.0",
1606
+ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0"
1607
+ }
1608
+ },
1609
  "node_modules/@modelcontextprotocol/sdk": {
1610
  "version": "1.26.0",
1611
  "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
frontend/package.json CHANGED
@@ -9,6 +9,7 @@
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
 
12
  "class-variance-authority": "^0.7.1",
13
  "clsx": "^2.1.1",
14
  "lucide-react": "^0.575.0",
 
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
12
+ "@marsidev/react-turnstile": "^1.4.2",
13
  "class-variance-authority": "^0.7.1",
14
  "clsx": "^2.1.1",
15
  "lucide-react": "^0.575.0",