diff --git a/.env.example b/.env.example index ba05429..d80aab2 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,9 @@ HOST=0.0.0.0 PORT=8000 LOG_LEVEL=info +# Authentication Configuration +ENABLE_AUTH=false # Set to true in production for JWT validation + # CORS Configuration # Comma-separated list of allowed origins # Use * to allow all origins (not recommended for production) @@ -42,12 +45,14 @@ EMSAL_TIMEOUT=60 # Sentry DSN for error tracking # SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id -# Clerk Configuration -CLERK_SECRET_KEY=sk_test_YourClerkSecretKey +# ------ Clerk ------ +CLERK_PUBLISHABLE_KEY=pk_test_xxx +CLERK_SECRET_KEY=sk_test_xxx +CLERK_ISSUER=https://your-app.clerk.accounts.dev # issuer & JWKS root -# Stripe Configuration -STRIPE_SECRET_KEY=sk_test_YourStripeSecretKey -STRIPE_WEBHOOK_SECRET=whsec_YourStripeWebhookSecret +# ------ Stripe ------ +STRIPE_SECRET=sk_live_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx # Application Configuration APP_URL=http://localhost:8000 diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..b0c246e --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 3a7c116..39f90cf 100644 --- a/.gitignore +++ b/.gitignore @@ -180,9 +180,10 @@ redis-data/ # Production logs logs/*.log.* -Dockerfile -Dockerfile -fly.toml -.github/workflows/fly-deploy.yml -Dockerfile + +# Remove these lines - we need deployment files in git: +# Dockerfile - NEEDED for SaaS deployment +# fly.toml - NEEDED for Fly.io deployment +# .github/workflows/fly-deploy.yml - NEEDED for GitHub Actions + GEMINI.md diff --git a/Dockerfile b/Dockerfile index 619ef7d..ad76258 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,33 @@ -# ---------- temel imaj ---------- -FROM python:3.12-slim - -# Playwright’in istediği kitaplıklar -RUN apt-get update && apt-get install -y \ - libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 \ - libdrm2 libdbus-1-3 libatspi2.0-0 libx11-6 libxcomposite1 \ - libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libxcb1 \ - libxkbcommon0 libpango-1.0-0 libcairo2 libasound2 \ - && rm -rf /var/lib/apt/lists/* +# -------- BASE IMAGE (includes Chromium & deps) ---------------------------- +FROM mcr.microsoft.com/playwright/python:v1.52.0-jammy # Playwright docs +# -------- Runtime setup ---------------------------------------------------- WORKDIR /app -# Gereksinimler -COPY requirements.txt . -RUN pip install --no-cache-dir --upgrade pip -RUN pip install --no-cache-dir -r requirements.txt +# Copy dependency manifests first for layer-cache +COPY pyproject.toml poetry.lock* requirements*.txt* ./ -# Uygulama dosyaları +# Fast, deterministic install with `uv` +RUN pip install --no-cache-dir uv && \ + uv pip install --system --no-cache-dir .[asgi] # installs FastMCP, FastAPI + +# Copy application source COPY . . -# Python path ayarı (import sorunlarını önler) -ENV PYTHONPATH=/app - -# Playwright tarayıcılarını kur -RUN playwright install chromium - +# -------- Environment ------------------------------------------------------ ENV PYTHONUNBUFFERED=1 +ENV ENABLE_AUTH=true # Clerk JWT validation ON by default +ENV PORT=8000 + +# -------- Health check ----------------------------------------------------- +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD python - <<'PY' +import httpx, os, sys; \ +r=httpx.get(f"http://localhost:{os.getenv('PORT','8000')}/health"); \ +sys.exit(0 if r.status_code==200 else 1) +PY + EXPOSE 8000 -CMD ["uvicorn", "fastapi_app:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"] +# -------- Entrypoint ------------------------------------------------------- +CMD ["uvicorn", "asgi_app:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"] \ No newline at end of file diff --git a/asgi_app.py b/asgi_app.py index 007ca22..fe90e23 100644 --- a/asgi_app.py +++ b/asgi_app.py @@ -18,7 +18,9 @@ from starlette.requests import Request from starlette.responses import JSONResponse, PlainTextResponse # Import the main MCP app -from mcp_server_main import app as mcp_server +from mcp_factory import create_app + +mcp_server = create_app() # Add a health check endpoint @mcp_server.custom_route("/health", methods=["GET"]) @@ -83,6 +85,9 @@ custom_middleware = [ ), ] +# Import Stripe webhook router +from stripe_webhook import router as stripe_router + # Create ASGI apps with different transports # Recommended: Streamable HTTP transport @@ -91,6 +96,9 @@ app = mcp_server.http_app( middleware=custom_middleware ) +# Add Stripe webhook router +app.include_router(stripe_router, prefix="/api") + # Alternative: SSE transport (for compatibility) sse_app = mcp_server.http_app( path="/sse", diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..4752970 --- /dev/null +++ b/fly.toml @@ -0,0 +1,32 @@ +# fly.toml app configuration file generated for yargi-mcp on 2025-06-29T00:23:47+03:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'yargi-mcp' +primary_region = 'fra' + +[env] +ENABLE_AUTH = "true" +PORT = "8000" + +[build] + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 + +[checks.http_health] # keep MCP /health live + type = "http" + interval = "30s" + timeout = "10s" + path = "/health" diff --git a/mcp_factory.py b/mcp_factory.py new file mode 100644 index 0000000..93f326e --- /dev/null +++ b/mcp_factory.py @@ -0,0 +1,28 @@ +import os +from functools import lru_cache +from fastmcp import FastMCP +from fastmcp.server.auth import BearerAuthProvider + +@lru_cache +def create_app() -> FastMCP: + """Return a FastMCP instance; Clerk JWT validation when ENABLE_AUTH=true.""" + if os.getenv("ENABLE_AUTH", "false").lower() != "true": + return FastMCP( + name="Yargı MCP – DEV", + instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK, Sayistay, Rekabet).", + dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"] + ) + + issuer = os.environ["CLERK_ISSUER"] # e.g. https://cool-app.clerk.accounts.dev + auth = BearerAuthProvider( + jwks_uri=f"{issuer}/.well-known/jwks.json", # Clerk JWKS pattern + issuer=issuer, + audience=os.environ["CLERK_PUBLISHABLE_KEY"], # PK appears in aud claim + required_scopes=["yargi.read"], + ) + return FastMCP( + name="Yargı MCP – PROD", + auth=auth, + instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK, Sayistay, Rekabet) with JWT authentication.", + dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"] + ) \ No newline at end of file diff --git a/mcp_server_main.py b/mcp_server_main.py index 1931ce0..04bbf39 100644 --- a/mcp_server_main.py +++ b/mcp_server_main.py @@ -31,7 +31,7 @@ root_logger.addHandler(console_handler) logger = logging.getLogger(__name__) # --- Logging Configuration End --- -from fastmcp import FastMCP +from mcp_factory import create_app # --- Module Imports --- from yargitay_mcp_module.client import YargitayOfficialApiClient @@ -97,11 +97,7 @@ from sayistay_mcp_module.models import ( from sayistay_mcp_module.enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum -app = FastMCP( - name="YargiMCP", - instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK, Sayistay).", - dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"] -) +app = create_app() # --- API Client Instances --- yargitay_client_instance = YargitayOfficialApiClient() diff --git a/stripe_webhook.py b/stripe_webhook.py new file mode 100644 index 0000000..c49838a --- /dev/null +++ b/stripe_webhook.py @@ -0,0 +1,24 @@ +import os, stripe +from clerk import ClerkClient # Clerk backend SDK +from fastapi import APIRouter, Request, HTTPException + +router = APIRouter() +stripe.api_key = os.getenv("STRIPE_SECRET") +clerk = ClerkClient(secret_key=os.getenv("CLERK_SECRET_KEY")) + +@router.post("/stripe/webhook") +async def stripe_hook(req: Request): + payload, sig = await req.body(), req.headers["stripe-signature"] + try: + event = stripe.Webhook.construct_event( # Stripe-recommended verify + payload, sig, os.getenv("STRIPE_WEBHOOK_SECRET")) + except stripe.error.SignatureVerificationError: + raise HTTPException(400, "Bad sig") + + if event["type"] == "customer.subscription.updated": + item = event["data"]["object"]["items"]["data"][0] + plan = item["price"]["nickname"] # "Pro", "Enterprise"… + userID = event["data"]["object"]["metadata"]["clerk_user_id"] + clerk.users.update_user_metadata( # merge into unsafe_metadata + userID, unsafe_metadata={"plan": plan}) + return {"ok": True} \ No newline at end of file