Skip to content
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ VITE_API_URL=
# Set to "true" locally. NEVER set in Vercel env vars.
VITE_DEV_MODE=true

# Cloudflare Turnstile
VITE_TURNSTILE_SITE_KEY=

# ── PostHog Analytics ─────────────────────────────────────────────────────────
# Leave blank locally — PostHog is silently disabled when this key is missing.
# Set both vars in your Vercel / production environment dashboard.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/greetings.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Greetings
name: Greetings
Comment thread
jpdevhub marked this conversation as resolved.

on:
pull_request_target:
Expand Down
25 changes: 24 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,27 @@ Models/
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?

# Additional local / secret ignores
# Local build outputs
build/

# Alternate virtualenv names
myenv/

# Docker local overrides and runtime artifacts
docker-compose.override.yml
.docker/

# Hugging Face / ML cache
.hf/
.cache/

# Secrets files
secrets.env
*.secret

# Local DB / artifacts
*.sqlite3
*.db
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ The setup script detects your environment automatically:
```env
VITE_API_URL= # leave blank for local dev; Vite proxy handles /api/*
VITE_DEV_MODE=true # enables DEV LOGIN button; never set true in production
VITE_TURNSTILE_SITE_KEY= # Cloudflare Turnstile site key
```

**Backend** — copy `backend/.env.example` to `backend/.env`:
Expand All @@ -109,9 +110,14 @@ SUPABASE_KEY=<auto-filled by npm run setup>
SUPABASE_SERVICE_KEY=
FRONTEND_URL=http://localhost:5173
API_BASE_URL=http://localhost:8000
TURNSTILE_SECRET_KEY=
DEV_BYPASS_AUTH=true # never set true in production
```

> Note: When using production auth flows, set `VITE_TURNSTILE_SITE_KEY` in the frontend and `TURNSTILE_SECRET_KEY` in the backend. This enables Cloudflare Turnstile protection for auth endpoint requests.

> Authentication start requests are rate limited in the backend to 5 requests per minute. Invalid or missing Turnstile tokens are rejected with a standard 400 response.

Comment on lines +117 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix blockquote formatting to satisfy markdownlint.

There is a blank line inside a blockquote block, which triggers MD028. Keep the blockquote contiguous.

Suggested patch
 > Note: When using production auth flows, set `VITE_TURNSTILE_SITE_KEY` in the frontend and `TURNSTILE_SECRET_KEY` in the backend. This enables Cloudflare Turnstile protection for auth endpoint requests.
-
 > Authentication start requests are rate limited in the backend to 5 requests per minute. Invalid or missing Turnstile tokens are rejected with a standard 400 response.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> Note: When using production auth flows, set `VITE_TURNSTILE_SITE_KEY` in the frontend and `TURNSTILE_SECRET_KEY` in the backend. This enables Cloudflare Turnstile protection for auth endpoint requests.
> Authentication start requests are rate limited in the backend to 5 requests per minute. Invalid or missing Turnstile tokens are rejected with a standard 400 response.
> Note: When using production auth flows, set `VITE_TURNSTILE_SITE_KEY` in the frontend and `TURNSTILE_SECRET_KEY` in the backend. This enables Cloudflare Turnstile protection for auth endpoint requests.
> Authentication start requests are rate limited in the backend to 5 requests per minute. Invalid or missing Turnstile tokens are rejected with a standard 400 response.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 118-118: Blank line inside blockquote

(MD028, no-blanks-blockquote)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 117 - 120, The blockquote in README.md contains a
blank line breaking the quote (triggering MD028); fix it by making the
blockquote contiguous: ensure each quoted line begins with ">" (e.g. for the two
paragraphs starting "Note: When using production auth flows..." and
"Authentication start requests...") and remove the empty line between them (or
combine them into a single continuous blockquote), so there are no blank lines
inside the blockquote.

Source: Linters/SAST tools

### Available Scripts

| Script | Description |
Expand Down
16 changes: 15 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,19 @@ API_BASE_URL=http://localhost:8000
DEV_BYPASS_AUTH=true
DEV_BYPASS_TOKEN=dev-local-bypass-token

# ── Cloudflare Turnstile — protect auth endpoints from automated abuse
TURNSTILE_SECRET_KEY=

# ── ML Models (optional) ──────────────────────────────────────────────────────
# Leave unset → demo mode (random scores, no PyTorch needed).
# Uncomment to enable real ML inference:
# MODEL_DIR=/absolute/path/to/Models
# STREAM_A_MODEL=/absolute/path/to/Models/freshscan_stream_a_body.pth
# STREAM_B_MODEL=/absolute/path/to/Models/stream_b_checkpoint.pth

# ── HF Hub (production HF Space only — set as Space secrets, NOT here) ────────
# HF_MODEL_REPO=karansingh12/freshscan-models
# MODEL_TOKEN=hf_xxxxxxxxxxxxxxxxxxxx
=========
# ── CORS ──────────────────────────────────────────────────────────────────────
CORS_ALLOW_ALL=true
CORS_ALLOW_ALL=true
Comment thread
jpdevhub marked this conversation as resolved.
56 changes: 47 additions & 9 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from contextlib import asynccontextmanager
from typing import Optional
from auth import get_current_user, get_google_oauth_url, exchange_code_for_session
from turnstile import TURNSTILE_SECRET_KEY, verify_turnstile_token
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from rate_limiter import limiter


# Load .env file if present (python-dotenv)
try:
from dotenv import load_dotenv
Expand All @@ -19,9 +21,10 @@
except ImportError:
pass

from fastapi import FastAPI, File, Request, UploadFile, Form, HTTPException, Depends, Query
from fastapi import Body, FastAPI, File, UploadFile, Form, HTTPException, Depends, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.responses import RedirectResponse, JSONResponse
from slowapi import _rate_limit_exceeded_handler
from supabase import create_client, Client
from PIL import Image

Expand All @@ -35,7 +38,6 @@
_torch_available = False
print("WARNING: PyTorch not installed. Scan endpoints will return 503.")


# ── Configuration ─────────────────────────────────────────────────────────────
# All secrets MUST come from environment variables — no hardcoded fallbacks.
SUPABASE_URL = os.environ.get("SUPABASE_URL", "")
Expand Down Expand Up @@ -93,8 +95,6 @@ async def lifespan(app: FastAPI):
)
yield

from fastapi import FastAPI

app = FastAPI(title="FreshScan AI", version="1.1.0", lifespan=lifespan)

_cors_origins = (
Expand Down Expand Up @@ -123,6 +123,7 @@ async def lifespan(app: FastAPI):
allow_headers=["*"],
)
app.state.limiter = limiter
app.add_exception_handler(429, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)

@app.exception_handler(RateLimitExceeded)
Expand Down Expand Up @@ -370,12 +371,44 @@ async def api_health_check():
"""Health check endpoint — no auth or DB required."""
return {"status": "ok"}

@app.get("/api/v1/auth/login/google")
async def login_google():
def _auth_redirect_url() -> str:
callback_url = f"{API_BASE_URL}/api/v1/auth/callback"
return get_google_oauth_url(redirect_to=callback_url)


async def _verify_turnstile(turnstile_token: str | None, request: Request) -> None:
if TURNSTILE_SECRET_KEY:
client_host = request.client.host if request.client else None
await verify_turnstile_token(turnstile_token, client_host)

Comment thread
jpdevhub marked this conversation as resolved.

@app.get("/api/v1/auth/login/google")
@limiter.limit("5/minute")
async def login_google_get(
request: Request,
turnstile_token: str | None = Query(None, alias="turnstile_token"),
):
try:
url = get_google_oauth_url(redirect_to=callback_url)
return RedirectResponse(url=url)
await _verify_turnstile(turnstile_token, request)
return RedirectResponse(url=_auth_redirect_url())
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Could not generate OAuth URL: {exc}")
Comment thread
jpdevhub marked this conversation as resolved.


@app.post("/api/v1/auth/login/google")
@limiter.limit("5/minute")
async def login_google_post(
Comment thread
jpdevhub marked this conversation as resolved.
request: Request,
payload: dict | None = Body(None),
):
turnstile_token = payload.get("turnstile_token") if payload else None
try:
await _verify_turnstile(turnstile_token, request)
return {"redirect_url": _auth_redirect_url()}
Comment thread
jpdevhub marked this conversation as resolved.
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Could not generate OAuth URL: {exc}")

Expand Down Expand Up @@ -539,6 +572,9 @@ async def scan_auto(
from router import classify_image_type, ImageType

img = Image.open(io.BytesIO(image_bytes)).convert("RGB")



image_type = classify_image_type(img)

if image_type == ImageType.NOT_A_FISH:
Expand Down Expand Up @@ -868,6 +904,7 @@ async def generate_gradcam(
from inference import stream_a_model, stream_a_transforms, device
from router import is_valid_fish_image


# Fish validity gate — same gate used by /api/v1/scan-auto
is_fish, gate_score = is_valid_fish_image(img_pil)
print(f" [GradCAM] Fish gate: {'PASS' if is_fish else 'FAIL'} (score={gate_score:.2%})")
Expand Down Expand Up @@ -956,3 +993,4 @@ def _bwd_hook(_module, _grad_in, grad_out):

register_routes(vendors_router, _db)
app.include_router(vendors_router)

3 changes: 2 additions & 1 deletion backend/requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ numpy<2.0.0
python-dotenv>=1.0.0
python-multipart>=0.0.29
httpx>=0.27.0
slowapi==0.1.9 # rate limiting for FastAPI; added for per-user scan endpoint throttling
slowapi==0.1.9 # rate limiting for FastAPI; added for per-user scan endpoint throttling

Loading
Loading