Spaces:
Sleeping
Sleeping
File size: 7,086 Bytes
ce8f04a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | """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
@router.post("/developer/keys")
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}
@router.get("/developer/keys")
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)}
@router.delete("/developer/keys/{key_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}
@public_router.get("/openapi.json")
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": []}],
}
@public_router.get("/health")
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(),
}
@public_router.get("/grants/nabory")
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
],
}
@public_router.get("/trust/summary")
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(),
}
@public_router.get("/regional/programs")
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,
}
|