diff --git a/frontend/package.json b/frontend/package.json index 206a727..6341394 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,9 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.22.0", "@stellar/freighter-api": "^2.0.0", - "@stellar/stellar-sdk": "^12.0.0" + "@stellar/stellar-sdk": "^12.0.0", + "i18next": "^23.11.5", + "react-i18next": "^14.1.2" }, "devDependencies": { "@vitejs/plugin-react": "^4.2.1", diff --git a/frontend/src/components/LanguageSelector.jsx b/frontend/src/components/LanguageSelector.jsx new file mode 100644 index 0000000..698f65c --- /dev/null +++ b/frontend/src/components/LanguageSelector.jsx @@ -0,0 +1,39 @@ +import { useTranslation } from 'react-i18next'; + +const LANGUAGES = [ + { code: 'en', label: 'EN' }, + { code: 'fr', label: 'FR' }, +]; + +export default function LanguageSelector() { + const { i18n } = useTranslation(); + + const handleChange = (code) => { + i18n.changeLanguage(code); + localStorage.setItem('lang', code); + }; + + return ( +
- Transaction hash not available for this record. -
+{t('modal.noTxHash')}
)}Access denied: issuer role required.
{t('issuer.accessDenied')}
- Blockchain-based vaccination records on Stellar — soulbound, verifiable, tamper-proof. -
+{t('landing.subtitle')}
{publicKey ? ( <>- ✅ Connected: {publicKey.slice(0, 8)}…{publicKey.slice(-4)} + {t('landing.connected', { address: `${publicKey.slice(0, 8)}…${publicKey.slice(-4)}` })}
)} -Requires Freighter browser extension on Stellar Testnet.
+{t('landing.requiresFreighter')}
@@ -80,10 +83,10 @@ export default function PatientDashboard() { disabled={page === 1} aria-label="Previous page" > - ‹ Prev + {t('patient.prevPage')} - Page {page} of {totalPages} + {t('patient.pageOf', { page, total: totalPages })} )} diff --git a/frontend/src/pages/VerifyPage.jsx b/frontend/src/pages/VerifyPage.jsx index a159dcf..ad490e3 100644 --- a/frontend/src/pages/VerifyPage.jsx +++ b/frontend/src/pages/VerifyPage.jsx @@ -58,7 +58,7 @@ export default function VerifyPage() { setWallet(e.target.value)} aria-label="Stellar wallet address to verify" diff --git a/python-service/main.py b/python-service/main.py index 5468791..62c3116 100644 --- a/python-service/main.py +++ b/python-service/main.py @@ -3,6 +3,7 @@ from fastapi import FastAPI, Request from routes.analytics import router as analytics_router from routes.batch import router as batch_router +from schemas import HealthResponse structlog.configure( processors=[ @@ -32,4 +33,4 @@ async def log_requests(request: Request, call_next): @app.get("/health") def health(): - return {"status": "ok"} + return HealthResponse(status="ok") diff --git a/python-service/routes/analytics.py b/python-service/routes/analytics.py index 43a11ca..6fc153b 100644 --- a/python-service/routes/analytics.py +++ b/python-service/routes/analytics.py @@ -3,12 +3,13 @@ from fastapi import APIRouter, HTTPException import httpx -router = APIRouter() +router = APIRouter(tags=["Analytics"]) BACKEND_URL = os.getenv("BACKEND_URL", "http://backend:4000") # Anomaly threshold: flag issuers with more than this many mints in the dataset ANOMALY_THRESHOLD = int(os.getenv("ANOMALY_THRESHOLD", "50")) +_bearer = HTTPBearer(description="JWT issued by the VacciChain backend via POST /auth/verify") async def _fetch_events(event_type: str, limit: int = 500) -> list: async with httpx.AsyncClient() as client: diff --git a/python-service/routes/batch.py b/python-service/routes/batch.py index 85d0b9e..fc8e882 100644 --- a/python-service/routes/batch.py +++ b/python-service/routes/batch.py @@ -1,37 +1,45 @@ -import httpx import os -from fastapi import APIRouter -from pydantic import BaseModel -from typing import List +import httpx +from fastapi import APIRouter, HTTPException +from schemas import BatchVerifyRequest, BatchVerifyResponse, WalletResult -router = APIRouter() +router = APIRouter(tags=["Batch"]) BACKEND_URL = os.getenv("BACKEND_URL", "http://backend:4000") -class BatchVerifyRequest(BaseModel): - wallets: List[str] - - -@router.post("/verify") +@router.post( + "/verify", + response_model=BatchVerifyResponse, + summary="Bulk verify Stellar wallet vaccination status", + description=( + "Accepts up to **100** Stellar public-key addresses and returns the vaccination " + "status for each one by querying the on-chain verification endpoint.\n\n" + "- Each address must be a valid Stellar public key starting with `G`.\n" + "- Wallets that cannot be reached are returned with an `error` field instead of " + "`vaccinated`/`record_count`.\n\n" + "**Auth:** No authentication required — mirrors the public `/verify/:wallet` endpoint." + ), + responses={ + 400: {"description": "Request contains more than 100 wallets"}, + }, +) async def batch_verify(request: BatchVerifyRequest): - """Bulk verify a list of Stellar wallet addresses.""" if len(request.wallets) > 100: - from fastapi import HTTPException raise HTTPException(status_code=400, detail="Maximum 100 wallets per request") - results = [] + results: list[WalletResult] = [] async with httpx.AsyncClient() as client: for wallet in request.wallets: try: res = await client.get(f"{BACKEND_URL}/verify/{wallet}", timeout=10) data = res.json() - results.append({ - "wallet": wallet, - "vaccinated": data.get("vaccinated", False), - "record_count": data.get("record_count", 0), - }) + results.append(WalletResult( + wallet=wallet, + vaccinated=data.get("vaccinated", False), + record_count=data.get("record_count", 0), + )) except Exception as e: - results.append({"wallet": wallet, "error": str(e)}) + results.append(WalletResult(wallet=wallet, error=str(e))) - return {"results": results, "total": len(results)} + return BatchVerifyResponse(results=results, total=len(results)) diff --git a/python-service/schemas.py b/python-service/schemas.py new file mode 100644 index 0000000..78d0c6b --- /dev/null +++ b/python-service/schemas.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel, Field +from typing import List, Optional + + +# ── Health ──────────────────────────────────────────────────────────────────── + +class HealthResponse(BaseModel): + status: str = Field(..., description="Service liveness status", examples=["ok"]) + + +# ── Analytics ───────────────────────────────────────────────────────────────── + +class VaccinationRatesResponse(BaseModel): + note: str = Field(..., description="Data-source note for this placeholder response") + sample: dict = Field( + ..., + description="Map of vaccine name → total administrations", + examples=[{"COVID-19": 1240, "Influenza": 870}], + ) + + +class IssuerStat(BaseModel): + issuer: str = Field(..., description="Stellar public key of the issuer", examples=["GABC...XYZ"]) + total_issued: int = Field(..., description="Total vaccination NFTs minted by this issuer", ge=0) + last_active: str = Field(..., description="ISO-8601 date of most recent mint", examples=["2024-03-15"]) + + +class IssuerActivityResponse(BaseModel): + note: str = Field(..., description="Data-source note for this placeholder response") + sample: List[IssuerStat] + + +class AnomalyResponse(BaseModel): + note: str = Field(..., description="Detection methodology description") + flagged_issuers: List[str] = Field( + ..., + description="Stellar addresses of issuers flagged for unusual mint volume (>50 mints/hour)", + ) + + +# ── Batch ───────────────────────────────────────────────────────────────────── + +class BatchVerifyRequest(BaseModel): + wallets: List[str] = Field( + ..., + description="List of Stellar public-key addresses to verify (max 100)", + min_length=1, + max_length=100, + examples=[["GABC...XYZ", "GDEF...UVW"]], + ) + + +class WalletResult(BaseModel): + wallet: str = Field(..., description="Stellar public-key address that was checked") + vaccinated: Optional[bool] = Field(None, description="True if at least one valid vaccination record exists") + record_count: Optional[int] = Field(None, description="Number of vaccination records found", ge=0) + error: Optional[str] = Field(None, description="Error message if this wallet could not be verified") + + +class BatchVerifyResponse(BaseModel): + results: List[WalletResult] = Field(..., description="Per-wallet verification results") + total: int = Field(..., description="Number of wallets processed", ge=0)