diff --git a/backend/app/core/config/settings.py b/backend/app/core/config/settings.py index 1349a02f..4139c410 100644 --- a/backend/app/core/config/settings.py +++ b/backend/app/core/config/settings.py @@ -14,6 +14,7 @@ class Settings(BaseSettings): # Platforms github_token: str = "" + github_webhook_secret: str = "" discord_bot_token: str = "" # DB configuration @@ -39,6 +40,14 @@ class Settings(BaseSettings): # Backend URL backend_url: str = "" + # CORS configuration + allow_origins: list[str] = [ + "http://localhost:5173", + "http://localhost:3000", + "http://127.0.0.1:5173", + "http://127.0.0.1:3000", + ] + # Onboarding UX toggles onboarding_show_oauth_button: bool = True diff --git a/backend/main.py b/backend/main.py index b7ad80a6..8df9e156 100644 --- a/backend/main.py +++ b/backend/main.py @@ -107,17 +107,23 @@ async def lifespan(app: FastAPI): # Configure CORS api.add_middleware( CORSMiddleware, - allow_origins=[ - "http://localhost:5173", # Vite default dev server - "http://localhost:3000", # Alternative dev server - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ], + allow_origins=settings.allow_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) + +@api.exception_handler(Exception) +async def global_exception_handler(request, exc): + """Log all unhandled exceptions and return a JSON error response.""" + logger.error(f"Unhandled exception: {str(exc)}", exc_info=True) + return Response( + content='{"detail": "Internal Server Error"}', + status_code=500, + media_type="application/json" + ) + @api.get("/favicon.ico") async def favicon(): """Return empty favicon to prevent 404 logs""" diff --git a/backend/routes.py b/backend/routes.py index 7dbd6463..f38cd137 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -1,12 +1,15 @@ -import asyncio -import uuid +import hmac +import hashlib import logging -from fastapi import APIRouter, Request, HTTPException +import uuid +import json +from fastapi import APIRouter, Request, HTTPException, Header from app.core.events.event_bus import EventBus from app.core.events.enums import EventType, PlatformType from app.core.events.base import BaseEvent from app.core.handler.handler_registry import HandlerRegistry from pydantic import BaseModel +from app.core.config.settings import settings router = APIRouter() @@ -35,50 +38,74 @@ def register_event_handlers(): event_bus.register_handler(EventType.PR_COMMENTED, sample_handler, PlatformType.GITHUB) event_bus.register_handler(EventType.PR_MERGED, sample_handler, PlatformType.GITHUB) +async def verify_signature(body: bytes, x_hub_signature_256: str): + """Verify that the payload was sent from GitHub by validating the SHA256 signature.""" + if not settings.github_webhook_secret: + # If secret is not configured, we skip verification (not recommended for production) + return + + if not x_hub_signature_256: + raise HTTPException(status_code=401, detail="X-Hub-Signature-256 header is missing") + + signature = hmac.new( + settings.github_webhook_secret.encode(), + body, + hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(f"sha256={signature}", x_hub_signature_256): + raise HTTPException(status_code=401, detail="Invalid webhook signature") + @router.post("/github/webhook") -async def github_webhook(request: Request): - payload = await request.json() - event_header = request.headers.get("X-GitHub-Event") - logging.info(f"Received GitHub event: {event_header}") +async def github_webhook( + request: Request, + x_github_event: str = Header(None), + x_hub_signature_256: str = Header(None) +): + # Read body once to avoid stream exhaustion + body = await request.body() + + # Verify GitHub signature + await verify_signature(body, x_hub_signature_256) + + try: + payload = json.loads(body.decode()) + except json.JSONDecodeError: + raise HTTPException(status_code=400, detail="Invalid JSON payload") from None + logging.info(f"Received GitHub event: {x_github_event}") event_type = None - # Handle issue events - if event_header == "issues": - action = payload.get("action") - if action == "opened": - event_type = EventType.ISSUE_CREATED - elif action == "closed": - event_type = EventType.ISSUE_CLOSED - elif action == "edited": - event_type = EventType.ISSUE_UPDATED - - # Handle issue comment events - elif event_header == "issue_comment": - action = payload.get("action") - if action == "created": - event_type = EventType.ISSUE_COMMENTED + # Mapping GitHub events and actions to EventType + event_mapping = { + "issues": { + "opened": EventType.ISSUE_CREATED, + "closed": EventType.ISSUE_CLOSED, + "edited": EventType.ISSUE_UPDATED, + }, + "issue_comment": { + "created": EventType.ISSUE_COMMENTED, + }, + "pull_request": { + "opened": EventType.PR_CREATED, + "edited": EventType.PR_UPDATED, + }, + "pull_request_review_comment": { + "created": EventType.PR_COMMENTED, + }, + } - # Handle pull request events - elif event_header == "pull_request": + if x_github_event in event_mapping: action = payload.get("action") - if action == "opened": - event_type = EventType.PR_CREATED - elif action == "edited": - event_type = EventType.PR_UPDATED - elif action == "closed": - # Determine if the PR was merged or simply closed + event_type = event_mapping[x_github_event].get(action) + + # Special casing for PR merged + if x_github_event == "pull_request" and action == "closed": if payload.get("pull_request", {}).get("merged"): event_type = EventType.PR_MERGED else: logging.info("Pull request closed without merge; no event dispatched.") - # Handle pull request comment events - elif event_header in ["pull_request_review_comment", "pull_request_comment"]: - action = payload.get("action") - if action == "created": - event_type = EventType.PR_COMMENTED - # Dispatch the event if we have a matching type if event_type: event = BaseEvent( @@ -90,6 +117,6 @@ async def github_webhook(request: Request): ) await event_bus.dispatch(event) else: - logging.info(f"No matching event type for header: {event_header} with action: {payload.get('action')}") + logging.info(f"No matching event type for header: {x_github_event} with action: {payload.get('action')}") return {"status": "ok"} diff --git a/frontend/Devr logo.png b/frontend/Devr logo.png new file mode 100644 index 00000000..f54690d8 Binary files /dev/null and b/frontend/Devr logo.png differ diff --git a/frontend/index.html b/frontend/index.html index 87106d51..6521557f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,16 @@ -
- - - -