86 lines
3.0 KiB
Python
86 lines
3.0 KiB
Python
"""Public consumer verification — no auth. Single-read via the QR snapshot.
|
|
|
|
GET /verify/{code} is READ-ONLY (no scan logging) so renders, prefetches, bots
|
|
and React Strict Mode double-mounts never inflate the scan count.
|
|
The frontend logs a scan explicitly via POST /scans/log after the page mounts.
|
|
|
|
Must return valid data in Active, Grace, and Archived subscription states;
|
|
never 404 for a valid QR. Resolves the four states:
|
|
genuine | unverified | not_found | recalled.
|
|
"""
|
|
from fastapi import APIRouter, Request
|
|
|
|
from app.models import QRCode, Organization, ScanLog
|
|
from app.models.documents import now
|
|
from app.models.enums import QRStatus, VerificationState, SubscriptionStatus
|
|
|
|
router = APIRouter(tags=["verify"])
|
|
|
|
|
|
def _device_from_ua(ua: str) -> str:
|
|
ua = (ua or "").lower()
|
|
if "android" in ua:
|
|
return "Android"
|
|
if "iphone" in ua or "ipad" in ua or "ios" in ua:
|
|
return "iOS"
|
|
if "mobile" in ua:
|
|
return "Other"
|
|
return "Desktop"
|
|
|
|
|
|
def _resolve_state(qr: QRCode, product: dict) -> VerificationState:
|
|
if product.get("recalled"):
|
|
return VerificationState.recalled
|
|
if qr.status in (QRStatus.disabled, QRStatus.inactive, QRStatus.expired):
|
|
return VerificationState.unverified
|
|
return VerificationState.genuine
|
|
|
|
|
|
@router.get("/verify/{qr_code}")
|
|
async def verify(qr_code: str):
|
|
"""Read-only. Does NOT log a scan."""
|
|
qr = await QRCode.find_one(QRCode.code == qr_code)
|
|
if not qr:
|
|
return {"state": VerificationState.not_found,
|
|
"message": "This QR code is not registered with VerifyPack."}
|
|
|
|
org = await Organization.get(qr.org_id)
|
|
if org and org.subscription_status == SubscriptionStatus.deleted:
|
|
return {"state": VerificationState.not_found,
|
|
"message": "This product record is no longer available."}
|
|
|
|
snap = qr.snapshot or {}
|
|
product = snap.get("product", {})
|
|
state = _resolve_state(qr, product)
|
|
|
|
return {
|
|
"state": state,
|
|
"qr_code": qr.code,
|
|
"serial": qr.serial,
|
|
"brand": snap.get("brand"),
|
|
"product": product,
|
|
"batch": snap.get("batch"),
|
|
"trust_score": product.get("trust_score", 90),
|
|
"subscription_state": org.subscription_status if org else None,
|
|
"powered_by": "VerifyPack",
|
|
}
|
|
|
|
|
|
@router.post("/scans/log")
|
|
async def log_scan(qr_code: str, request: Request):
|
|
"""Record a single scan. Called once by the consumer page after it mounts."""
|
|
qr = await QRCode.find_one(QRCode.code == qr_code)
|
|
if not qr:
|
|
return {"logged": False}
|
|
ua = request.headers.get("user-agent", "")
|
|
try:
|
|
await ScanLog(org_id=qr.org_id, qr_id=str(qr.id), qr_code=qr.code,
|
|
product_id=qr.product_id, device=_device_from_ua(ua),
|
|
ip=request.client.host if request.client else None).insert()
|
|
qr.scan_count += 1
|
|
qr.last_scan_at = now()
|
|
await qr.save()
|
|
except Exception:
|
|
return {"logged": False}
|
|
return {"logged": True, "scan_count": qr.scan_count}
|