Spaces:
Running
Running
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",
|