diff --git a/.gitignore b/.gitignore index 4c4563d..9b1ee87 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules .pnp.js myenv/ venv/ - +*.md # testing coverage diff --git a/backend/main.py b/backend/main.py index 066ad1d..30e1bb8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,8 +2,11 @@ Protly Backend — FastAPI server for ESMFold protein structure prediction. Endpoints: - GET /api/health → health check - POST /api/predict → accepts { sequence: str }, returns PDB + pLDDT data + GET /api/health → health check + POST /api/predict → accepts { sequence: str }, returns PDB + pLDDT data + GET /api/uniprot/search → proxy UniProt search + GET /api/uniprot/entry/{id} → fetch full UniProt entry + POST /api/analyze → Biopython lab-readiness metrics """ import re @@ -22,7 +25,6 @@ from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded -from jose import jwt, JWTError from dotenv import load_dotenv load_dotenv() @@ -46,15 +48,6 @@ app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) -# --------------------------------------------------------------------------- -# Supabase Auth Config -# --------------------------------------------------------------------------- -SUPABASE_URL = os.getenv("SUPABASE_URL", "") -SUPABASE_JWT_SECRET = os.getenv("SUPABASE_JWT_SECRET", "") -SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY", "") - -if not SUPABASE_JWT_SECRET: - logger.warning("SUPABASE_JWT_SECRET is not set — JWT auth verification will be skipped!") # --------------------------------------------------------------------------- # CORS — allow the Vite dev server and common local origins @@ -68,7 +61,7 @@ allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["GET", "POST", "OPTIONS"], - allow_headers=["Authorization", "Content-Type", "X-Requested-With"], + allow_headers=["Content-Type", "X-Requested-With"], ) @@ -88,55 +81,6 @@ async def add_security_headers(request: Request, call_next): return response -# --------------------------------------------------------------------------- -# JWT Authentication middleware -# --------------------------------------------------------------------------- -# Endpoints that do NOT require authentication -PUBLIC_PATHS = {"/api/health", "/docs", "/openapi.json", "/redoc"} - - -@app.middleware("http") -async def authenticate_requests(request: Request, call_next): - """Validate Supabase JWT on protected endpoints.""" - path = request.url.path - - # Skip auth for public endpoints and CORS preflight - if path in PUBLIC_PATHS or request.method == "OPTIONS": - return await call_next(request) - - # If no JWT secret configured, skip validation (dev mode) - if not SUPABASE_JWT_SECRET: - return await call_next(request) - - # Extract Bearer token - auth_header = request.headers.get("Authorization", "") - if not auth_header.startswith("Bearer "): - return JSONResponse( - status_code=401, - content={"detail": "Missing or invalid Authorization header. Please sign in."}, - ) - - token = auth_header.split(" ", 1)[1] - - try: - payload = jwt.decode( - token, - SUPABASE_JWT_SECRET, - algorithms=["HS256"], - audience="authenticated", - ) - # Attach user info to request state for downstream handlers - request.state.user_id = payload.get("sub") - request.state.user_email = payload.get("email", "") - except JWTError as exc: - logger.warning("JWT verification failed: %s", exc) - return JSONResponse( - status_code=401, - content={"detail": "Invalid or expired token. Please sign in again."}, - ) - - return await call_next(request) - # --------------------------------------------------------------------------- # Request logging middleware diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4f8beac..75e577b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -134,7 +134,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -497,7 +496,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -546,7 +544,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1856,7 +1853,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2019,7 +2017,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2030,7 +2027,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2206,7 +2202,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2247,6 +2242,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2355,7 +2351,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2433,7 +2428,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -2715,7 +2709,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/electron-to-chromium": { "version": "1.5.302", @@ -2825,7 +2820,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3526,6 +3520,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -3752,7 +3747,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3821,6 +3815,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3836,6 +3831,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -3858,7 +3854,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3878,7 +3873,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3898,7 +3892,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -3975,8 +3968,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4411,7 +4403,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4718,7 +4709,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index aa8d081..beff4e6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,4 @@ import { useState, useCallback, useRef } from 'react'; -import { useAuth } from './components/AuthProvider'; -import LoginPage from './components/LoginPage'; import Sidebar from './components/Sidebar'; import TopBar from './components/TopBar'; import MolViewer from './components/MolViewer'; @@ -32,16 +30,6 @@ const DEFAULT_FILTERS = { let toastId = 0; export default function App() { - const { session, user, loading, signOut } = useAuth(); - - // ---- auth helpers ---- - const authHeaders = useCallback(() => { - const headers = { 'Content-Type': 'application/json' }; - if (session?.access_token) { - headers['Authorization'] = `Bearer ${session.access_token}`; - } - return headers; - }, [session]); // ---- view state ---- const [view, setView] = useState('dashboard'); // 'dashboard' | 'discovery' | 'analysis' @@ -97,7 +85,7 @@ export default function App() { try { const res = await fetch(`${API_BASE}/api/predict`, { method: 'POST', - headers: authHeaders(), + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sequence: seq.trim() }), }); @@ -118,7 +106,7 @@ export default function App() { addToast('error', err.message); } }, - [sequence, addToast, authHeaders] + [sequence, addToast] ); // ---- UniProt search ---- @@ -145,7 +133,7 @@ export default function App() { }); const res = await fetch(`${API_BASE}/api/uniprot/search?${params}`, { - headers: authHeaders(), + headers: { 'Content-Type': 'application/json' }, }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); @@ -164,7 +152,7 @@ export default function App() { setSearchLoading(false); } }, - [filters, addToast, authHeaders] + [filters, addToast] ); const handleFiltersChange = useCallback((updater) => { @@ -197,7 +185,7 @@ export default function App() { try { const entryRes = await fetch(`${API_BASE}/api/uniprot/entry/${accession}`, { - headers: authHeaders(), + headers: { 'Content-Type': 'application/json' }, }); if (!entryRes.ok) { const errBody = await entryRes.json().catch(() => ({})); @@ -222,12 +210,12 @@ export default function App() { const [predictRes, analyzeRes] = await Promise.all([ fetch(`${API_BASE}/api/predict`, { method: 'POST', - headers: authHeaders(), + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sequence: protein.sequence }), }), fetch(`${API_BASE}/api/analyze`, { method: 'POST', - headers: authHeaders(), + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sequence: protein.sequence }), }), ]); @@ -257,7 +245,7 @@ export default function App() { addToast('error', err.message); } }, - [addToast, authHeaders] + [addToast] ); // ---- navigation ---- @@ -285,33 +273,9 @@ export default function App() { URL.revokeObjectURL(url); }, [pdbData, selectedProtein]); - // Show loading spinner during auth check - if (loading) { - return ( -
-
-
-
-
-
-
-

Loading Protly…

-
-
- ); - } - - // Redirect to login when not authenticated - if (!session) { - return ; - } - return (
- +
{/* ========== DASHBOARD VIEW ========== */} diff --git a/frontend/src/components/ActionsCard.jsx b/frontend/src/components/ActionsCard.jsx index 6a3fdc1..7ff442b 100644 --- a/frontend/src/components/ActionsCard.jsx +++ b/frontend/src/components/ActionsCard.jsx @@ -26,14 +26,7 @@ export default function ActionsCard({ pdbData, onDownload, status, sequence }) { Export & Actions
-
- - -
+
diff --git a/frontend/src/components/AuthProvider.jsx b/frontend/src/components/AuthProvider.jsx deleted file mode 100644 index 515e11d..0000000 --- a/frontend/src/components/AuthProvider.jsx +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable react-refresh/only-export-components */ -import { createContext, useContext, useEffect, useState } from 'react'; -import { supabase } from '../lib/supabaseClient'; - -const AuthContext = createContext({ - session: null, - user: null, - loading: true, - signInWithGoogle: async () => {}, - signOut: async () => {}, -}); - -export function useAuth() { - return useContext(AuthContext); -} - -export default function AuthProvider({ children }) { - const [session, setSession] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - // Get current session on mount - supabase.auth.getSession().then(({ data: { session: s } }) => { - setSession(s); - setLoading(false); - }); - - // Listen for auth state changes (login, logout, token refresh) - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, s) => { - setSession(s); - setLoading(false); - }); - - return () => subscription.unsubscribe(); - }, []); - - const signInWithGoogle = async () => { - const { error } = await supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - redirectTo: window.location.origin, - }, - }); - if (error) throw error; - }; - - const signOut = async () => { - const { error } = await supabase.auth.signOut(); - if (error) throw error; - }; - - const user = session?.user ?? null; - - return ( - - {children} - - ); -} diff --git a/frontend/src/components/GeneInfo.jsx b/frontend/src/components/GeneInfo.jsx index f05ec03..c2b2f5a 100644 --- a/frontend/src/components/GeneInfo.jsx +++ b/frontend/src/components/GeneInfo.jsx @@ -13,14 +13,7 @@ export default function GeneInfo({ status }) { Gene & Organism
-
- - -
+
diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx deleted file mode 100644 index 0a1bf5b..0000000 --- a/frontend/src/components/LoginPage.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useState } from 'react'; -import { useAuth } from './AuthProvider'; - -export default function LoginPage() { - const { signInWithGoogle } = useAuth(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const handleGoogleLogin = async () => { - setIsLoading(true); - setError(null); - try { - await signInWithGoogle(); - } catch (err) { - setError(err.message || 'Failed to sign in with Google'); - setIsLoading(false); - } - }; - - return ( -
- {/* Floating orbs background */} -
-
-
-
-
- -
- {/* Logo */} -
-
P
- - Protly. - -
- -

Welcome Back

-

- Sign in to access the Protein Folding Analysis Dashboard -

- - {error && ( -
- - {error} -
- )} - - - -
- Secure Authentication -
- -
-
- 🔒 -
- End-to-End Security -

OAuth 2.0 via Supabase with encrypted sessions

-
-
-
- 🧬 -
- Protein Analysis -

ESMFold structure prediction & visualization

-
-
-
- 🔍 -
- UniProt Discovery -

Search & analyze millions of protein entries

-
-
-
-
-
- ); -} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 6b0daa1..13e496c 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,19 +1,4 @@ -import { useState } from 'react'; - -export default function Sidebar({ activeView, onViewChange, user, onSignOut }) { - const [showMenu, setShowMenu] = useState(false); - - const initials = user?.user_metadata?.full_name - ? user.user_metadata.full_name - .split(' ') - .map((n) => n[0]) - .join('') - .slice(0, 2) - .toUpperCase() - : user?.email?.[0]?.toUpperCase() || 'U'; - - const avatarUrl = user?.user_metadata?.avatar_url; - +export default function Sidebar({ activeView, onViewChange }) { return ( ); } diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index 9d0dc80..543b11b 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -1,17 +1,14 @@ -import { useState, useRef, useEffect } from 'react'; +<<<<<<< HEAD +======= +>>>>>>> 4f90b14c2b714a2a63ce06e05a9d26745477f34f export default function TopBar({ onSearch, searchQuery, setSearchQuery, view, onBackToSearch, - user, - onSignOut, }) { - const [showUserMenu, setShowUserMenu] = useState(false); - const menuRef = useRef(null); - const handleKeyDown = (e) => { if (e.key === 'Enter' && searchQuery.trim()) { onSearch(searchQuery.trim()); @@ -24,26 +21,6 @@ export default function TopBar({ } }; - // Close menu on outside click - useEffect(() => { - const handleClick = (e) => { - if (menuRef.current && !menuRef.current.contains(e.target)) { - setShowUserMenu(false); - } - }; - if (showUserMenu) document.addEventListener('mousedown', handleClick); - return () => document.removeEventListener('mousedown', handleClick); - }, [showUserMenu]); - - const avatarUrl = user?.user_metadata?.avatar_url; - const displayName = user?.user_metadata?.full_name || user?.email || 'User'; - const initials = displayName - .split(' ') - .map((n) => n[0]) - .join('') - .slice(0, 2) - .toUpperCase(); - return (
@@ -65,15 +42,6 @@ export default function TopBar({ Discovery )} - - Schedule - - - History - - - Activity -
@@ -129,93 +97,6 @@ export default function TopBar({ )}
)} - - - - {/* User avatar with dropdown */} -
- - - {showUserMenu && ( -
-
- {avatarUrl && ( - - )} -
-
{displayName}
-
{user?.email}
-
-
-
- -
- )} -
); diff --git a/frontend/src/lib/supabaseClient.js b/frontend/src/lib/supabaseClient.js deleted file mode 100644 index 620f731..0000000 --- a/frontend/src/lib/supabaseClient.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Supabase Client — singleton used across the React app for auth & database. - * - * Reads config from Vite environment variables: - * VITE_SUPABASE_URL → Supabase project URL - * VITE_SUPABASE_ANON_KEY → Supabase anon / public key - */ - -import { createClient } from '@supabase/supabase-js'; - -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; - -if (!supabaseUrl || !supabaseAnonKey) { - console.warn( - '[Supabase] Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY — auth features will not work.' - ); -} - -export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || ''); diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index e10d79b..d9cc93c 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,12 +2,9 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App.jsx'; -import AuthProvider from './components/AuthProvider'; createRoot(document.getElementById('root')).render( - - - + );