Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/app/core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Settings(BaseSettings):

# Platforms
github_token: str = ""
github_webhook_secret: str = ""
discord_bot_token: str = ""

# DB configuration
Expand All @@ -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

Expand Down
18 changes: 12 additions & 6 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
101 changes: 64 additions & 37 deletions backend/routes.py
Original file line number Diff line number Diff line change
@@ -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()

Expand Down Expand Up @@ -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
Comment on lines +43 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add warning log when signature verification is skipped.

Silently skipping signature verification creates a security risk. If accidentally deployed to production without a configured secret, the endpoint will accept unauthenticated webhook requests from any source.

🔎 Proposed fix
     if not settings.github_webhook_secret:
         # If secret is not configured, we skip verification (not recommended for production)
+        logging.warning("GitHub webhook secret not configured - signature verification skipped. This is insecure for production!")
         return
🤖 Prompt for AI Agents
In @backend/routes.py around lines 43 - 45, When the code hits the condition "if
not settings.github_webhook_secret" it currently returns silently; add a
warning-level log immediately before the return to surface that signature
verification is being skipped (e.g., logger.warning or app.logger.warning with a
clear message like "GitHub webhook secret not configured; skipping signature
verification — insecure for production."). Update the webhook handler code where
the settings.github_webhook_secret check lives to emit this warning and include
identifying context (environment or endpoint) using the existing logger
instance, then return as before.


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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate required webhook headers explicitly.

The x_github_event header can be None, but there's no explicit validation. While the code handles None gracefully downstream (it won't match any event mapping), validating required headers upfront provides clearer error messages and better API hygiene.

🔎 Proposed fix
 async def github_webhook(
     request: Request,
-    x_github_event: str = Header(None),
+    x_github_event: str = Header(...),
     x_hub_signature_256: str = Header(None)
 ):

Using Header(...) instead of Header(None) makes x_github_event required, and FastAPI will automatically return a clear 422 error if the header is missing.

🤖 Prompt for AI Agents
In @backend/routes.py at line 62, The x_github_event header parameter is
currently optional (Header(None)); make it required by replacing Header(None)
with Header(...) in the route signature where x_github_event: str is declared
(so FastAPI will validate and return a 422 when the header is missing), ensuring
the webhook handler parameter name x_github_event is updated in that function
signature and any callers/tests adjusted if needed.

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(
Expand All @@ -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"}
Binary file added frontend/Devr logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 14 additions & 11 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Devr.AI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/DevrAI.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Devr.AI</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
File renamed without changes
140 changes: 43 additions & 97 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
import toast, { Toaster } from 'react-hot-toast';
import { Toaster } from 'react-hot-toast';

import Sidebar from './components/layout/Sidebar';
import Dashboard from './components/dashboard/Dashboard';
Expand All @@ -15,112 +15,50 @@ import LandingPage from './components/landing/LandingPage';
import LoginPage from './components/pages/LoginPage';
import ProfilePage from './components/pages/ProfilePage';
import SignUpPage from './components/pages/SignUpPage';
import { supabase } from './lib/supabaseClient';
import ForgotPasswordPage from './components/pages/ForgotPasswordPage';
import ResetPasswordPage from './components/pages/ResetPasswordPage';
import { useAuth } from './hooks/useAuth';
import { RepositoryData } from './types';

// Define proper TypeScript interfaces
interface RepositoryData {
id: string;
name: string;
full_name: string;
description?: string;
html_url: string;
stargazers_count: number;
forks_count: number;
language?: string;
created_at: string;
updated_at: string;
owner: {
login: string;
avatar_url: string;
};
interface ProtectedLayoutProps {
isSidebarOpen: boolean;
setIsSidebarOpen: (isOpen: boolean) => void;
handleLogout: () => Promise<void>;
}

const ProtectedLayout = ({ isSidebarOpen, setIsSidebarOpen, handleLogout }: ProtectedLayoutProps) => (
<div className="flex">
<Sidebar
isOpen={isSidebarOpen}
setIsOpen={setIsSidebarOpen}
onLogout={handleLogout}
/>
<main
className={`transition-all duration-300 flex-1 ${isSidebarOpen ? 'ml-64' : 'ml-20'
}`}
>
<div className="p-8">
<AnimatePresence mode="wait">
<Outlet />
</AnimatePresence>
</div>
</main>
</div>
);

function App() {
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [repoData, setRepoData] = useState<RepositoryData | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);

// Auto login if user has already logged in
useEffect(() => {
supabase.auth.getSession().then(({ data, error }) => {
if (error) {
toast.error('User Login Failed');
console.error('Error checking session:', error);
return;
}
setIsAuthenticated(!!data.session);
});

const { data: subscription } = supabase.auth.onAuthStateChange(
(event, session) => {
switch (event) {
case "SIGNED_IN":
setIsAuthenticated(true);
toast.success("Signed in!");
break;

case "SIGNED_OUT":
setIsAuthenticated(false);
setRepoData(null);
toast.success("Signed out!");
break;

case "PASSWORD_RECOVERY":
toast("Check your email to reset your password.");
break;
case "TOKEN_REFRESHED":
// Session refreshed silently
break;
case "USER_UPDATED":
// User profile updated
break;
}
}
);
const { isAuthenticated, loading, logout } = useAuth();

return () => {
subscription.subscription.unsubscribe();
};
}, []);

const handleLogin = () => {
setIsAuthenticated(true);
};
if (loading) {
return <div className="min-h-screen bg-gray-950 flex items-center justify-center text-white">Loading...</div>;
}

const handleLogout = async () => {
const { error } = await supabase.auth.signOut();
if (error) {
toast.error('Logout failed');
console.error('Error during logout:', error);
return;
}
toast.success('Signed out!');
setIsAuthenticated(false);
setRepoData(null);
await logout();
};

const ProtectedLayout = () => (
<div className="flex">
<Sidebar
isOpen={isSidebarOpen}
setIsOpen={setIsSidebarOpen}
onLogout={handleLogout}
/>
<main
className={`transition-all duration-300 flex-1 ${
isSidebarOpen ? 'ml-64' : 'ml-20'
}`}
>
<div className="p-8">
<AnimatePresence mode="wait">
<Outlet />
</AnimatePresence>
</div>
</main>
</div>
);

return (
<Router>
Expand All @@ -133,7 +71,7 @@ function App() {
isAuthenticated ? (
<Navigate to="/" replace />
) : (
<LoginPage onLogin={handleLogin} />
<LoginPage />
)
}
/>
Expand All @@ -157,7 +95,15 @@ function App() {
<Route
path="/"
element={
isAuthenticated ? <ProtectedLayout /> : <Navigate to="/login" replace />
isAuthenticated ? (
<ProtectedLayout
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
handleLogout={handleLogout}
/>
) : (
<Navigate to="/login" replace />
)
}
>
<Route index element={<LandingPage setRepoData={setRepoData} />} />
Expand Down
Loading