Spaces:
Sleeping
Sleeping
| """Public API v2 — API keys, rate-limited read-only endpoints.""" | |
| from __future__ import annotations | |
| from datetime import datetime, timezone | |
| from typing import Any, Optional | |
| from fastapi import APIRouter, Depends, Header, HTTPException, Query | |
| from pydantic import BaseModel, Field | |
| from sqlalchemy.orm import Session | |
| from core.subscription.api_keys import ( | |
| create_api_key, | |
| list_api_keys, | |
| revoke_api_key, | |
| verify_api_key_hash, | |
| ) | |
| from core.subscription.middleware import verify_token | |
| from endpoints.projects import get_db | |
| router = APIRouter(prefix="/api/v2", tags=["public-api-v2"]) | |
| public_router = APIRouter(prefix="/api/v2/public", tags=["public-api-v2"]) | |
| class ApiKeyCreateBody(BaseModel): | |
| label: str = Field(..., min_length=1, max_length=80) | |
| def _tenant_id(token_data: dict) -> str: | |
| return token_data.get("sub") or token_data.get("user_id") or "anonymous" | |
| async def require_api_key( | |
| x_api_key: Optional[str] = Header(None, alias="X-API-Key"), | |
| db: Session = Depends(get_db), | |
| ) -> dict[str, Any]: | |
| if not x_api_key or len(x_api_key) < 16: | |
| raise HTTPException(status_code=401, detail="Brak lub nieprawidłowy nagłówek X-API-Key") | |
| record = verify_api_key_hash(db, x_api_key) | |
| if not record: | |
| raise HTTPException(status_code=401, detail="Nieprawidłowy klucz API") | |
| return record | |
| async def issue_api_key( | |
| body: ApiKeyCreateBody, | |
| token_data: dict = Depends(verify_token), | |
| db: Session = Depends(get_db), | |
| ): | |
| clerk_id = _tenant_id(token_data) | |
| raw_key, meta = create_api_key(db, clerk_id, body.label) | |
| return {"api_key": raw_key, "key": meta} | |
| async def get_api_keys( | |
| token_data: dict = Depends(verify_token), | |
| db: Session = Depends(get_db), | |
| ): | |
| clerk_id = _tenant_id(token_data) | |
| return {"keys": list_api_keys(db, clerk_id)} | |
| async def delete_api_key( | |
| key_id: str, | |
| token_data: dict = Depends(verify_token), | |
| db: Session = Depends(get_db), | |
| ): | |
| clerk_id = _tenant_id(token_data) | |
| if not revoke_api_key(db, key_id, clerk_id): | |
| raise HTTPException(status_code=404, detail="Klucz nie znaleziony") | |
| return {"status": "revoked", "id": key_id} | |
| async def public_openapi_spec(): | |
| """Minimal OpenAPI subset for public integrators.""" | |
| return { | |
| "openapi": "3.1.0", | |
| "info": { | |
| "title": "GrantForge Public API v2", | |
| "version": "2.0.0", | |
| "description": "Read-only endpoints for grant catalog and trust signals.", | |
| }, | |
| "servers": [{"url": "/api/v2/public"}], | |
| "paths": { | |
| "/grants/nabory": { | |
| "get": { | |
| "summary": "List grant calls", | |
| "parameters": [ | |
| {"name": "q", "in": "query", "schema": {"type": "string"}}, | |
| {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 20}}, | |
| ], | |
| "responses": {"200": {"description": "Grant list"}}, | |
| } | |
| }, | |
| "/trust/summary": { | |
| "get": { | |
| "summary": "Platform trust score summary", | |
| "responses": {"200": {"description": "Trust summary"}}, | |
| } | |
| }, | |
| "/regional/programs": { | |
| "get": { | |
| "summary": "Verified regional RPO programs", | |
| "parameters": [ | |
| { | |
| "name": "voivodeship", | |
| "in": "query", | |
| "schema": {"type": "string"}, | |
| }, | |
| { | |
| "name": "limit", | |
| "in": "query", | |
| "schema": {"type": "integer", "default": 50}, | |
| }, | |
| ], | |
| "responses": {"200": {"description": "Regional program list"}}, | |
| } | |
| }, | |
| "/health": { | |
| "get": { | |
| "summary": "API health", | |
| "responses": {"200": {"description": "OK"}}, | |
| } | |
| }, | |
| }, | |
| "components": { | |
| "securitySchemes": { | |
| "ApiKeyAuth": {"type": "apiKey", "in": "header", "name": "X-API-Key"} | |
| } | |
| }, | |
| "security": [{"ApiKeyAuth": []}], | |
| } | |
| async def public_health(_key: dict = Depends(require_api_key)): | |
| return { | |
| "status": "ok", | |
| "version": "2.0.0", | |
| "authenticated_as": _key.get("user_id"), | |
| "checked_at": datetime.now(timezone.utc).isoformat(), | |
| } | |
| async def public_grants_nabory( | |
| q: Optional[str] = Query(None), | |
| limit: int = Query(20, ge=1, le=100), | |
| _key: dict = Depends(require_api_key), | |
| db: Session = Depends(get_db), | |
| ): | |
| from core.grants.models import Grant | |
| query = db.query(Grant) | |
| if q: | |
| like = f"%{q.strip()}%" | |
| query = query.filter(Grant.name.ilike(like) | Grant.operator.ilike(like)) | |
| rows = query.order_by(Grant.fetched_at.desc()).limit(limit).all() | |
| return { | |
| "count": len(rows), | |
| "nabory": [ | |
| { | |
| "id": g.id, | |
| "name": g.name, | |
| "operator": g.operator, | |
| "status": g.status, | |
| "regions": g.eligible_regions, | |
| "url": g.url or g.official_page_url, | |
| } | |
| for g in rows | |
| ], | |
| } | |
| async def public_trust_summary( | |
| _key: dict = Depends(require_api_key), | |
| db: Session = Depends(get_db), | |
| ): | |
| from core.trust.trust_center import get_trust_center_summary | |
| summary = get_trust_center_summary(db) | |
| return { | |
| "platform_score": summary.get("platform_score"), | |
| "level": summary.get("level"), | |
| "components": summary.get("components"), | |
| "recommendations": summary.get("recommendations", [])[:5], | |
| "checked_at": datetime.now(timezone.utc).isoformat(), | |
| } | |
| async def public_regional_programs( | |
| voivodeship: Optional[str] = Query(None, max_length=50), | |
| limit: int = Query(50, ge=1, le=200), | |
| _key: dict = Depends(require_api_key), | |
| ): | |
| from core.grants.regional_catalog import get_voivodeships, list_regional_programs | |
| from core.grants.rpo_bip_parsers import get_cached_bip_programs, list_bip_voivodeships | |
| programs = list_regional_programs(voivodeship=voivodeship, serveable_only=True)[:limit] | |
| bip_live = get_cached_bip_programs(voivodeship)[: max(0, limit - len(programs))] | |
| return { | |
| "count": len(programs) + len(bip_live), | |
| "programs": programs, | |
| "bip_live": bip_live, | |
| "voivodeships": get_voivodeships(), | |
| "bip_parser_voivodeships": list_bip_voivodeships(), | |
| "credibility_filtered": True, | |
| } | |