From 8a58641eb64d736dbef4309085804f2254f9ad57 Mon Sep 17 00:00:00 2001 From: "PRODHOSH V.S" Date: Sat, 7 Mar 2026 12:08:14 +0530 Subject: [PATCH 1/4] fix: startup auto-ingest, commit FAISS index, allow onrender.com in extension, un-ignore index dir --- .gitignore | 4 ++-- flashfetch-extension/manifest.json | 3 ++- flashfetch-extension/popup.js | 4 +++- rag-backend/index/chunks.pkl | Bin 0 -> 8312 bytes rag-backend/index/faiss.index | Bin 0 -> 6189 bytes rag-backend/main.py | 27 +++++++++++++++++++++++++++ 6 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 rag-backend/index/chunks.pkl create mode 100644 rag-backend/index/faiss.index diff --git a/.gitignore b/.gitignore index b178b93..26101aa 100644 --- a/.gitignore +++ b/.gitignore @@ -45,8 +45,8 @@ next-env.d.ts __pycache__/ *.pyc -# RAG backend generated index (rebuilt on deploy) -rag-backend/index/ +# RAG backend FAISS index — auto-rebuilt on startup from committed docs +# rag-backend/index/ ← intentionally NOT ignored so index is committed diff --git a/flashfetch-extension/manifest.json b/flashfetch-extension/manifest.json index 580ae79..12d77c7 100644 --- a/flashfetch-extension/manifest.json +++ b/flashfetch-extension/manifest.json @@ -35,7 +35,8 @@ "notifications" ], "host_permissions": [ - "https://rag-document-qa-bot-production.up.railway.app/*", + "https://*.onrender.com/*", + "https://*.vercel.app/*", "http://localhost:8000/*", "file:///*" ] diff --git a/flashfetch-extension/popup.js b/flashfetch-extension/popup.js index cc1292b..abeaf20 100644 --- a/flashfetch-extension/popup.js +++ b/flashfetch-extension/popup.js @@ -1,5 +1,7 @@ // Constants -const DEFAULT_API = "http://localhost:8000"; +// In production: update this to your Render backend URL +// e.g. "https://flashfetch-backend.onrender.com" +const DEFAULT_API = "http://localhost:8000"; const OLD_RAILWAY = "https://rag-document-qa-bot-production.up.railway.app"; // State diff --git a/rag-backend/index/chunks.pkl b/rag-backend/index/chunks.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2f26166bb1e6d23ed2bc8d89c7ccfa4e7751b2fb GIT binary patch literal 8312 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 WXP|4U#u#s2{||_|_?5JAX7F#dtocg- literal 0 HcmV?d00001 diff --git a/rag-backend/index/faiss.index b/rag-backend/index/faiss.index new file mode 100644 index 0000000000000000000000000000000000000000..ebf2770d382c76f0b4d5892a50d44f7112a74e77 GIT binary patch literal 6189 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 literal 0 HcmV?d00001 diff --git a/rag-backend/main.py b/rag-backend/main.py index 647b032..71efcdb 100644 --- a/rag-backend/main.py +++ b/rag-backend/main.py @@ -72,6 +72,33 @@ class AnswerResponse(BaseModel): confidence: str # "high" | "medium" | "low" +# ── Startup: auto-index committed docs ─────────────────── +@app.on_event("startup") +async def auto_ingest_on_startup(): + """ + On every cold start (Render free tier, local dev, etc.): + If docs exist but the FAISS index is missing, rebuild it automatically. + This ensures the demo docs committed to the repo are always indexed. + """ + index_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index", "faiss.index") + docs_exist = any( + f.endswith((".txt", ".md", ".pdf")) + for f in os.listdir(DOCS_DIR) + ) if os.path.isdir(DOCS_DIR) else False + + if docs_exist and not os.path.exists(index_path): + print("[startup] FAISS index missing — auto-building from docs...") + try: + run_ingest() + print("[startup] Index built successfully.") + except Exception as e: + print(f"[startup] WARNING: auto-ingest failed: {e}") + elif os.path.exists(index_path): + print("[startup] FAISS index found — skipping rebuild.") + else: + print("[startup] No docs found — upload a file to begin.") + + # ── Routes ──────────────────────────────────────────────── @app.post("/ask", response_model=AnswerResponse) def ask(query: Query): From d861838d6f413efc51ccbaa6015be3bcd7243f49 Mon Sep 17 00:00:00 2001 From: saireddy Date: Mon, 9 Mar 2026 19:41:34 +0530 Subject: [PATCH 2/4] fixed backend crashes and add the frontend signin button --- app/api/rag/[...path]/route.ts | 103 ++++++++++ app/chat/page.tsx | 119 +++++------ components/ui/auth-modal.tsx | 6 +- flashfetch-extension/background.js | 8 +- flashfetch-extension/popup.html | 7 +- flashfetch-extension/popup.js | 111 +++++----- middleware.ts | 15 +- package-lock.json | 5 +- package.json | 1 + rag-backend/docs/codes.txt | 1 + rag-backend/index/chunks.pkl | Bin 8312 -> 8361 bytes rag-backend/index/faiss.index | Bin 6189 -> 7725 bytes rag-backend/main.py | 87 +++++++- rag-backend/requirements.txt | 2 +- supabase/admin.sql | 318 ++++++++++++++--------------- 15 files changed, 490 insertions(+), 293 deletions(-) create mode 100644 app/api/rag/[...path]/route.ts create mode 100644 rag-backend/docs/codes.txt diff --git a/app/api/rag/[...path]/route.ts b/app/api/rag/[...path]/route.ts new file mode 100644 index 0000000..9e76744 --- /dev/null +++ b/app/api/rag/[...path]/route.ts @@ -0,0 +1,103 @@ +import { NextRequest, NextResponse } from "next/server"; +import { CookieOptions, createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +const RAG_API_URL = process.env.NEXT_PUBLIC_RAG_API_URL || "http://127.0.0.1:8000"; +const RAG_API_KEY = process.env.RAG_API_KEY || ""; + +export async function GET(request: NextRequest) { + return proxyRequest(request); +} + +export async function POST(request: NextRequest) { + return proxyRequest(request); +} + +export async function DELETE(request: NextRequest) { + return proxyRequest(request); +} + +async function proxyRequest(request: NextRequest) { + // 1. Verify Authentication + const cookieStore = await cookies(); + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) { + try { + cookiesToSet.forEach(({ name, value, options }) => { + cookieStore.set(name, value, options); + }); + } catch { + // The `set` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + } + ); + + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + return NextResponse.json({ detail: "Unauthorized" }, { status: 401 }); + } + + // 2. Construct Backend URL (extract the path segment after /api/rag/) + // e.g. /api/rag/documents -> documents + // e.g. /api/rag/documents/doc1 -> documents/doc1 + const match = request.nextUrl.pathname.match(/^\/api\/rag\/(.*)$/); + const path = match ? match[1] : ""; + + // Pass query parameters along to the backend + const queryString = request.nextUrl.search; + const backendUrl = `${RAG_API_URL}/${path}${queryString}`; + + // 3. Prepare headers + const headers = new Headers(); + headers.set("Authorization", `Bearer ${RAG_API_KEY}`); + + // Forward Content-Type if present (important for file uploads vs JSON) + const contentType = request.headers.get("Content-Type"); + if (contentType) { + headers.set("Content-Type", contentType); + } + + // 4. Forward the request + try { + const fetchOptions: RequestInit = { + method: request.method, + headers: headers, + // For GET/HEAD requests, body cannot be included + body: ["GET", "HEAD"].includes(request.method) ? undefined : await request.blob(), + // Disable caching for proxy requests + cache: "no-store", + }; + + const response = await fetch(backendUrl, fetchOptions); + + // 5. Stream the response back to the client + const responseHeaders = new Headers(response.headers); + // Remove headers that might cause issues when proxying + responseHeaders.delete("content-encoding"); + responseHeaders.delete("transfer-encoding"); + + return new NextResponse(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error: any) { + console.error("[Proxy Error]:", error); + return NextResponse.json( + { detail: "Backend connection failed" }, + { status: 502 } + ); + } +} diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 2d7996d..0eef925 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -18,40 +18,41 @@ import { type ChatSession, type SearchResult, } from "@/lib/chat-history"; -const RAG_API = process.env.NEXT_PUBLIC_RAG_API_URL ?? "http://localhost:8000"; -const DOCS_API = RAG_API + "/documents"; +// Use the local Next.js API proxy to securely attach the API key +const RAG_API = "/api/rag"; +const DOCS_API = RAG_API + "/documents"; const UPLOAD_API = RAG_API + "/upload"; const DELETE_API = (name: string) => `${RAG_API}/documents/${encodeURIComponent(name)}`; // ── Types ──────────────────────────────────────────────── -interface DocFile { name: string; size_kb: number } -interface Source { document: string; snippet: string; score: number } -interface Message { - id: string; - role: "user" | "assistant"; - content: string; - sources?: Source[]; +interface DocFile { name: string; size_kb: number } +interface Source { document: string; snippet: string; score: number } +interface Message { + id: string; + role: "user" | "assistant"; + content: string; + sources?: Source[]; confidence?: "high" | "medium" | "low"; - loading?: boolean; + loading?: boolean; } // ── PDF Export ─────────────────────────────────────────── async function exportAnswerPDF( - question: string, - answer: string, - sources: Source[], + question: string, + answer: string, + sources: Source[], confidence: string, ) { const { jsPDF } = await import("jspdf"); - const doc = new jsPDF({ unit: "mm", format: "a4" }); + const doc = new jsPDF({ unit: "mm", format: "a4" }); const pageW = doc.internal.pageSize.getWidth(); const margin = 18; - const maxW = pageW - margin * 2; - let y = 20; + const maxW = pageW - margin * 2; + let y = 20; const addText = ( - text: string, - size: number, + text: string, + size: number, style: "normal" | "bold" | "italic", color: [number, number, number] = [30, 30, 30], ) => { @@ -78,8 +79,8 @@ async function exportAnswerPDF( // Confidence pill const confColor: [number, number, number] = - confidence === "high" ? [16, 185, 129] : - confidence === "medium" ? [245, 158, 11] : [239, 68, 68]; + confidence === "high" ? [16, 185, 129] : + confidence === "medium" ? [245, 158, 11] : [239, 68, 68]; doc.setFillColor(...confColor); doc.roundedRect(margin, y - 4, 42, 6, 1.5, 1.5, "F"); doc.setFontSize(8); doc.setFont("helvetica", "bold"); doc.setTextColor(255, 255, 255); @@ -122,9 +123,9 @@ async function exportAnswerPDF( // ── Small components ───────────────────────────────────── function ConfidenceBadge({ level }: { level: "high" | "medium" | "low" }) { const map = { - high: { bar: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", dot: "bg-emerald-400", label: "High confidence" }, - medium: { bar: "bg-amber-500/15 text-amber-400 border-amber-500/30", dot: "bg-amber-400", label: "Medium confidence" }, - low: { bar: "bg-red-500/15 text-red-400 border-red-500/30", dot: "bg-red-400", label: "Low confidence" }, + high: { bar: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", dot: "bg-emerald-400", label: "High confidence" }, + medium: { bar: "bg-amber-500/15 text-amber-400 border-amber-500/30", dot: "bg-amber-400", label: "Medium confidence" }, + low: { bar: "bg-red-500/15 text-red-400 border-red-500/30", dot: "bg-red-400", label: "Low confidence" }, }; const c = map[level]; return ( @@ -163,7 +164,7 @@ function MessageBubble({ msg, onExport, }: { - msg: Message; + msg: Message; onExport?: (msg: Message) => void; }) { const isUser = msg.role === "user"; @@ -229,33 +230,33 @@ export default function ChatPage() { const router = useRouter(); // ── Auth ──────────────────────────────────────────────── - const [user, setUser] = React.useState(null); + const [user, setUser] = React.useState(null); const [authChecked, setAuthChecked] = React.useState(false); // ── Messages ──────────────────────────────────────────── const [messages, setMessages] = React.useState([]); - const [input, setInput] = React.useState(""); - const [sending, setSending] = React.useState(false); + const [input, setInput] = React.useState(""); + const [sending, setSending] = React.useState(false); // ── Documents ─────────────────────────────────────────── - const [docs, setDocs] = React.useState([]); + const [docs, setDocs] = React.useState([]); const [uploading, setUploading] = React.useState(false); - const [deleting, setDeleting] = React.useState(null); + const [deleting, setDeleting] = React.useState(null); // ── Sidebar / history ──────────────────────────────────── - const [sidebarTab, setSidebarTab] = React.useState<"files" | "history">("files"); - const [sessions, setSessions] = React.useState([]); - const [sessionId, setSessionId] = React.useState(null); + const [sidebarTab, setSidebarTab] = React.useState<"files" | "history">("files"); + const [sessions, setSessions] = React.useState([]); + const [sessionId, setSessionId] = React.useState(null); const [loadingHist, setLoadingHist] = React.useState(false); // ── Search ──────────────────────────────────────────────── - const [historySearch, setHistorySearch] = React.useState(""); - const [searchResults, setSearchResults] = React.useState([]); - const [isSearching, setIsSearching] = React.useState(false); + const [historySearch, setHistorySearch] = React.useState(""); + const [searchResults, setSearchResults] = React.useState([]); + const [isSearching, setIsSearching] = React.useState(false); // ── Share ───────────────────────────────────────────────── const [sharedSessions, setSharedSessions] = React.useState>(new Set()); - const [shareCopied, setShareCopied] = React.useState(null); + const [shareCopied, setShareCopied] = React.useState(null); // ── Voice ───────────────────────────────────────────────── const [listening, setListening] = React.useState(false); @@ -265,10 +266,10 @@ export default function ChatPage() { const [profileOpen, setProfileOpen] = React.useState(false); // ── Refs ──────────────────────────────────────────────── - const bottomRef = React.useRef(null); - const inputRef = React.useRef(null); + const bottomRef = React.useRef(null); + const inputRef = React.useRef(null); const fileInputRef = React.useRef(null); - const profileRef = React.useRef(null); + const profileRef = React.useRef(null); React.useEffect(() => { function handler(e: MouseEvent) { @@ -309,7 +310,7 @@ export default function ChatPage() { setSessions(s); } }); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [router]); React.useEffect(() => { fetchDocs(); }, []); @@ -343,10 +344,10 @@ export default function ChatPage() { setSessionId(sid); const msgs = await loadMessages(sid); const restored: Message[] = msgs.map((m) => ({ - id: m.id, - role: m.role as "user" | "assistant", - content: m.content, - sources: (m.sources as unknown as Source[]) ?? [], + id: m.id, + role: m.role as "user" | "assistant", + content: m.content, + sources: (m.sources as unknown as Source[]) ?? [], confidence: (m.confidence as "high" | "medium" | "low") ?? undefined, })); setMessages(restored); @@ -374,7 +375,7 @@ export default function ChatPage() { await shareSession(sid); setSharedSessions((prev) => new Set([...prev, sid])); const link = `${window.location.origin}/share/${sid}`; - await navigator.clipboard.writeText(link).catch(() => {}); + await navigator.clipboard.writeText(link).catch(() => { }); setShareCopied(sid); setTimeout(() => setShareCopied(null), 2500); } @@ -392,9 +393,9 @@ export default function ChatPage() { rec.continuous = false; rec.interimResults = true; rec.lang = "en-US"; - rec.onstart = () => setListening(true); - rec.onend = () => { setListening(false); inputRef.current?.focus(); }; - rec.onerror = () => setListening(false); + rec.onstart = () => setListening(true); + rec.onend = () => { setListening(false); inputRef.current?.focus(); }; + rec.onerror = () => setListening(false); rec.onresult = (e: any) => { const transcript = Array.from(e.results as any[]) .map((r: any) => r[0].transcript).join(""); @@ -419,7 +420,7 @@ export default function ChatPage() { const form = new FormData(); form.append("file", file); try { - const res = await fetch(UPLOAD_API, { method: "POST", body: form }); + const res = await fetch(UPLOAD_API, { method: "POST", body: form }); const data = await res.json(); if (!res.ok) throw new Error(data.detail ?? "Upload failed"); setDocs(data.documents ?? []); @@ -434,7 +435,7 @@ export default function ChatPage() { async function handleDelete(name: string) { setDeleting(name); try { - const res = await fetch(DELETE_API(name), { method: "DELETE" }); + const res = await fetch(DELETE_API(name), { method: "DELETE" }); const data = await res.json(); if (!res.ok) throw new Error(data.detail ?? "Delete failed"); setDocs(data.documents ?? []); @@ -448,7 +449,7 @@ export default function ChatPage() { // ── PDF export ──────────────────────────────────────────── function handleExport(msg: Message) { const idx = messages.findIndex((m) => m.id === msg.id); - const q = messages.slice(0, idx).filter((m) => m.role === "user").pop(); + const q = messages.slice(0, idx).filter((m) => m.role === "user").pop(); exportAnswerPDF(q?.content ?? "Question", msg.content, msg.sources ?? [], msg.confidence ?? "low"); } @@ -457,8 +458,8 @@ export default function ChatPage() { const question = input.trim(); if (!question || sending) return; - const userMsg: Message = { id: Date.now().toString(), role: "user", content: question }; - const placeholderId = Date.now().toString() + "-ai"; + const userMsg: Message = { id: Date.now().toString(), role: "user", content: question }; + const placeholderId = Date.now().toString() + "-ai"; const placeholder: Message = { id: placeholderId, role: "assistant", content: "", loading: true }; setMessages((prev) => [...prev, userMsg, placeholder]); @@ -485,9 +486,9 @@ export default function ChatPage() { try { const res = await fetch(RAG_API + "/ask", { - method: "POST", + method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ question, history }), + body: JSON.stringify({ question, history }), }); if (!res.ok) throw new Error(`API error ${res.status}`); const data = await res.json(); @@ -531,8 +532,8 @@ export default function ChatPage() { ); } - const displayName = user?.user_metadata?.full_name || user?.email?.split("@")[0] || "You"; - const avatarSrc = user ? resolveAvatar(user.id, user.user_metadata?.avatar_url) : null; + const displayName = user?.user_metadata?.full_name || user?.email?.split("@")[0] || "You"; + const avatarSrc = user ? resolveAvatar(user.id, user.user_metadata?.avatar_url) : null; const avatarLetter = (displayName as string)[0]?.toUpperCase() ?? "U"; return ( @@ -732,8 +733,8 @@ export default function ChatPage() { shareCopied === s.id ? "opacity-100 text-emerald-400" : sharedSessions.has(s.id) - ? "opacity-100 text-white/40 hover:text-amber-400" - : "text-white/15 hover:text-white/50 hover:bg-white/8", + ? "opacity-100 text-white/40 hover:text-amber-400" + : "text-white/15 hover:text-white/50 hover:bg-white/8", )} > {shareCopied === s.id ? : } diff --git a/components/ui/auth-modal.tsx b/components/ui/auth-modal.tsx index 991f35e..e3d12b4 100644 --- a/components/ui/auth-modal.tsx +++ b/components/ui/auth-modal.tsx @@ -204,9 +204,9 @@ export function AuthModal({ open, onOpenChange, onSuccess }: AuthModalProps) { } const titles: Record = { - login: { title: "Welcome back", desc: "Sign in to your account" }, + login: { title: "Welcome back", desc: "Sign in to your account" }, signup: { title: "Create an account", desc: "Join the TopDevs community" }, - forgot: { title: "Reset password", desc: "We'll send a reset link to your email" }, + forgot: { title: "Reset password", desc: "We'll send a reset link to your email" }, }; return ( @@ -295,7 +295,7 @@ export function AuthModal({ open, onOpenChange, onSuccess }: AuthModalProps) { - diff --git a/flashfetch-extension/background.js b/flashfetch-extension/background.js index 4f0951c..8ec3d48 100644 --- a/flashfetch-extension/background.js +++ b/flashfetch-extension/background.js @@ -28,8 +28,8 @@ chrome.contextMenus.onClicked.addListener((info, tab) => { } if (info.menuItemId === "flashfetch-save-page") { - chrome.storage.local.get(["ff_token", "ff_api_url"], async (data) => { - const apiUrl = data.ff_api_url || "https://rag-document-qa-bot-production.up.railway.app"; + chrome.storage.local.get(["ff_api_key", "ff_api_url"], async (data) => { + const apiUrl = data.ff_api_url || "http://localhost:8000"; // Inject content script to grab page text chrome.scripting.executeScript( @@ -49,7 +49,7 @@ chrome.contextMenus.onClicked.addListener((info, tab) => { try { const headers = {}; - if (data.ff_token) headers["Authorization"] = `Bearer ${data.ff_token}`; + if (data.ff_api_key) headers["Authorization"] = `Bearer ${data.ff_api_key}`; const res = await fetch(`${apiUrl}/upload`, { method: "POST", @@ -77,6 +77,6 @@ chrome.contextMenus.onClicked.addListener((info, tab) => { // Injected into the active tab to extract readable text function extractPageText() { const title = document.title; - const body = document.body ? document.body.innerText : ""; + const body = document.body ? document.body.innerText : ""; return { title, text: body.slice(0, 50000) }; // cap at 50 KB } diff --git a/flashfetch-extension/popup.html b/flashfetch-extension/popup.html index 3b5be87..6caafbc 100644 --- a/flashfetch-extension/popup.html +++ b/flashfetch-extension/popup.html @@ -13,11 +13,12 @@ FlashFetch -

Configure API URL (optional)

+

Setup your Backend

- + +
- + diff --git a/flashfetch-extension/popup.js b/flashfetch-extension/popup.js index abeaf20..0568bf9 100644 --- a/flashfetch-extension/popup.js +++ b/flashfetch-extension/popup.js @@ -1,25 +1,26 @@ // Constants // In production: update this to your Render backend URL // e.g. "https://flashfetch-backend.onrender.com" -const DEFAULT_API = "http://localhost:8000"; -const OLD_RAILWAY = "https://rag-document-qa-bot-production.up.railway.app"; +const DEFAULT_API = "http://localhost:8000"; +const OLD_RAILWAY = "https://rag-document-qa-bot-production.up.railway.app"; // State let conversationHistory = []; -let activeFileContext = null; +let activeFileContext = null; // DOM -const authScreen = document.getElementById("auth-screen"); -const chatScreen = document.getElementById("chat-screen"); -const apiUrlInput = document.getElementById("api-url-input"); -const saveTokenBtn = document.getElementById("save-token-btn"); -const messagesEl = document.getElementById("messages"); -const questionInput = document.getElementById("question-input"); -const sendBtn = document.getElementById("send-btn"); -const savePageBtn = document.getElementById("save-page-btn"); -const logoutBtn = document.getElementById("logout-btn"); -const statusBar = document.getElementById("status-bar"); -const fileBanner = document.getElementById("file-banner"); +const authScreen = document.getElementById("auth-screen"); +const chatScreen = document.getElementById("chat-screen"); +const apiUrlInput = document.getElementById("api-url-input"); +const apiKeyInput = document.getElementById("api-key-input"); +const saveTokenBtn = document.getElementById("save-token-btn"); +const messagesEl = document.getElementById("messages"); +const questionInput = document.getElementById("question-input"); +const sendBtn = document.getElementById("send-btn"); +const savePageBtn = document.getElementById("save-page-btn"); +const logoutBtn = document.getElementById("logout-btn"); +const statusBar = document.getElementById("status-bar"); +const fileBanner = document.getElementById("file-banner"); const fileBannerName = document.getElementById("file-banner-name"); const fileBannerClear = document.getElementById("file-banner-clear"); @@ -45,12 +46,12 @@ function detectOpenFile() { const tab = tabs[0]; if (!tab || !tab.url) return; - const url = tab.url; - const isFile = url.startsWith("file://"); - const rawName = url.split("/").pop().split("?")[0] || "document"; + const url = tab.url; + const isFile = url.startsWith("file://"); + const rawName = url.split("/").pop().split("?")[0] || "document"; const fileName = decodeURIComponent(rawName); - const isDrive = url.includes("drive.google.com") || url.includes("docs.google.com"); + const isDrive = url.includes("drive.google.com") || url.includes("docs.google.com"); const isPDFUrl = !isFile && url.toLowerCase().endsWith(".pdf"); if (isDrive || isPDFUrl) { @@ -85,16 +86,20 @@ function detectOpenFile() { // Load any URL via backend /extract-url function loadFromUrl(url, label) { - chrome.storage.local.get(["ff_api_url"], async (data) => { + chrome.storage.local.get(["ff_api_url", "ff_api_key"], async (data) => { const apiUrl = data.ff_api_url || DEFAULT_API; + const apiKey = data.ff_api_key || ""; clearEmptyState(); showLoadingBanner(label); setStatus("Reading " + label + "..."); try { + const headers = { "Content-Type": "application/json" }; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + const res = await fetch(apiUrl + "/extract-url", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers, body: JSON.stringify({ url }), }); @@ -106,8 +111,8 @@ function loadFromUrl(url, label) { const json = await res.json(); hideStatus(); setFileContext({ - title: json.filename, - text: json.text, + title: json.filename, + text: json.text, fileType: json.filename.toLowerCase().endsWith(".pdf") ? "pdf" : "txt", }); @@ -136,9 +141,9 @@ function setFileContext(res) { } function appendPDFHelp(fileName, errorMsg) { - const div = document.createElement("div"); + const div = document.createElement("div"); div.className = "msg assistant"; - const bubble = document.createElement("div"); + const bubble = document.createElement("div"); bubble.className = "bubble"; if (errorMsg && errorMsg.includes("Anyone with the link")) { @@ -186,8 +191,9 @@ function showChat() { saveTokenBtn.addEventListener("click", () => { const apiUrl = apiUrlInput.value.trim() || DEFAULT_API; - chrome.storage.local.set({ ff_api_url: apiUrl }, () => { - setStatus("API URL saved: " + apiUrl, "success"); + const apiKey = apiKeyInput.value.trim(); + chrome.storage.local.set({ ff_api_url: apiUrl, ff_api_key: apiKey }, () => { + setStatus("Settings saved!", "success"); setTimeout(() => hideStatus(), 2000); }); }); @@ -195,7 +201,7 @@ saveTokenBtn.addEventListener("click", () => { logoutBtn.addEventListener("click", () => { chrome.storage.local.remove(["ff_api_url"], () => { conversationHistory = []; - activeFileContext = null; + activeFileContext = null; setStatus("Reset done", "success"); setTimeout(() => hideStatus(), 1500); }); @@ -218,8 +224,9 @@ async function sendQuestion() { const question = questionInput.value.trim(); if (!question) return; - chrome.storage.local.get(["ff_api_url"], async (data) => { + chrome.storage.local.get(["ff_api_url", "ff_api_key"], async (data) => { const apiUrl = data.ff_api_url || DEFAULT_API; + const apiKey = data.ff_api_key || ""; clearEmptyState(); appendMessage("user", question); @@ -232,6 +239,7 @@ async function sendQuestion() { try { const headers = { "Content-Type": "application/json" }; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; let endpoint, body; @@ -240,8 +248,8 @@ async function sendQuestion() { body = JSON.stringify({ question, context_text: activeFileContext.text, - filename: activeFileContext.filename, - history: historyToSend.slice(0, -1), + filename: activeFileContext.filename, + history: historyToSend.slice(0, -1), }); } else { endpoint = apiUrl + "/ask"; @@ -256,10 +264,10 @@ async function sendQuestion() { if (!res.ok) throw new Error("API error " + res.status); - const json = await res.json(); - const answer = json.answer || "No answer returned."; + const json = await res.json(); + const answer = json.answer || "No answer returned."; const confidence = json.confidence || "low"; - const sources = json.sources || []; + const sources = json.sources || []; appendMessage("assistant", answer, confidence, sources); conversationHistory.push({ role: "assistant", content: answer }); @@ -276,30 +284,34 @@ async function sendQuestion() { // Save current page / upload savePageBtn.addEventListener("click", async () => { - chrome.storage.local.get(["ff_api_url"], async (data) => { + chrome.storage.local.get(["ff_api_url", "ff_api_key"], async (data) => { const apiUrl = data.ff_api_url || DEFAULT_API; + const apiKey = data.ff_api_key || ""; chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const tab = tabs[0]; if (!tab || !tab.url) { setStatus("Cannot read this tab", "error"); return; } - const url = tab.url; - const isPDF = url.toLowerCase().endsWith(".pdf"); - const isFile = url.startsWith("file://"); - const rawName = url.split("/").pop().split("?")[0] || "document"; + const url = tab.url; + const isPDF = url.toLowerCase().endsWith(".pdf"); + const isFile = url.startsWith("file://"); + const rawName = url.split("/").pop().split("?")[0] || "document"; const fileName = decodeURIComponent(rawName); if (isFile) { setStatus("Reading " + fileName + "..."); try { - const fileRes = await fetch(url); - const blob = await fileRes.blob(); + const fileRes = await fetch(url); + const blob = await fileRes.blob(); const mimeType = isPDF ? "application/pdf" : "text/plain"; - const upload = new Blob([blob], { type: mimeType }); + const upload = new Blob([blob], { type: mimeType }); const formData = new FormData(); formData.append("file", upload, fileName); setStatus("Uploading to FlashFetch..."); - const res = await fetch(apiUrl + "/upload", { method: "POST", body: formData }); + const headers = {}; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + + const res = await fetch(apiUrl + "/upload", { method: "POST", headers, body: formData }); if (!res.ok) throw new Error("Upload failed " + res.status); setStatus("Uploaded: " + fileName, "success"); clearEmptyState(); @@ -317,13 +329,16 @@ savePageBtn.addEventListener("click", async () => { setStatus("Cannot read this page", "error"); return; } const { title, text } = response; - const blob = new Blob([text], { type: "text/plain" }); - const fname = (title || "webpage").replace(/[^a-z0-9]/gi, "_").slice(0, 40) + ".txt"; + const blob = new Blob([text], { type: "text/plain" }); + const fname = (title || "webpage").replace(/[^a-z0-9]/gi, "_").slice(0, 40) + ".txt"; const formData = new FormData(); formData.append("file", blob, fname); setStatus("Uploading..."); try { - const res = await fetch(apiUrl + "/upload", { method: "POST", body: formData }); + const headers = {}; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + + const res = await fetch(apiUrl + "/upload", { method: "POST", headers, body: formData }); if (!res.ok) throw new Error("Upload failed " + res.status); setStatus("Saved: " + fname, "success"); setTimeout(() => hideStatus(), 3000); @@ -342,9 +357,9 @@ function clearEmptyState() { } function appendMessage(role, text, confidence, sources) { - const div = document.createElement("div"); + const div = document.createElement("div"); div.className = "msg " + role; - const bubble = document.createElement("div"); + const bubble = document.createElement("div"); bubble.className = "bubble"; bubble.textContent = text; div.appendChild(bubble); @@ -375,7 +390,7 @@ function appendMessage(role, text, confidence, sources) { let typingCounter = 0; function appendTyping() { - const id = "typing-" + (++typingCounter); + const id = "typing-" + (++typingCounter); const wrapper = document.createElement("div"); wrapper.className = "msg assistant"; wrapper.id = id; diff --git a/middleware.ts b/middleware.ts index 56d4074..dca65b2 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,4 +1,4 @@ -import { createServerClient } from "@supabase/ssr"; +import { CookieOptions, createServerClient } from "@supabase/ssr"; import { NextResponse, type NextRequest } from "next/server"; export async function middleware(request: NextRequest) { @@ -16,7 +16,7 @@ export async function middleware(request: NextRequest) { getAll() { return request.cookies.getAll(); }, - setAll(cookiesToSet) { + setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) { cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value) ); @@ -29,7 +29,16 @@ export async function middleware(request: NextRequest) { }); // Refresh the session — do NOT remove this line - await supabase.auth.getUser(); + const { data: { user } } = await supabase.auth.getUser(); + + // Protect /chat and /admin routes + const isProtectedRoute = request.nextUrl.pathname.startsWith('/chat') || request.nextUrl.pathname.startsWith('/admin'); + + if (isProtectedRoute && !user) { + const redirectUrl = request.nextUrl.clone(); + redirectUrl.pathname = '/'; + return NextResponse.redirect(redirectUrl); + } return supabaseResponse; } diff --git a/package-lock.json b/package-lock.json index 4444089..427136c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/trusted-types": "^2.0.7", "eslint": "^9", "eslint-config-next": "16.1.6", "shadcn": "^3.8.5", @@ -4192,8 +4193,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", diff --git a/package.json b/package.json index 67836c0..3cc43d3 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/trusted-types": "^2.0.7", "eslint": "^9", "eslint-config-next": "16.1.6", "shadcn": "^3.8.5", diff --git a/rag-backend/docs/codes.txt b/rag-backend/docs/codes.txt new file mode 100644 index 0000000..150008e --- /dev/null +++ b/rag-backend/docs/codes.txt @@ -0,0 +1 @@ +hi ra puka how are you \ No newline at end of file diff --git a/rag-backend/index/chunks.pkl b/rag-backend/index/chunks.pkl index 2f26166bb1e6d23ed2bc8d89c7ccfa4e7751b2fb..128581c704be484bb3ab167e4f2c4c6333735709 100644 GIT binary patch delta 128 zcmez2u+q`Bfo1AE1qLvPouW}YMWctMB(69L};{4L0fn{p00s|PtPSL2HqS3=rl3GzRWn<)RK8+r>;{4L0JY8;qPN+WZisQ=ol{tcLtj`V`-`$08L3DXaE2J diff --git a/rag-backend/index/faiss.index b/rag-backend/index/faiss.index index ebf2770d382c76f0b4d5892a50d44f7112a74e77..e715f6596cd9cc027f1d35d42db81cb464c7fb40 100644 GIT binary patch delta 6220 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 delta 4672 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 diff --git a/rag-backend/main.py b/rag-backend/main.py index 71efcdb..4d83934 100644 --- a/rag-backend/main.py +++ b/rag-backend/main.py @@ -17,8 +17,12 @@ import re import httpx import fitz # PyMuPDF +import ipaddress +import urllib.parse +import socket -from fastapi import FastAPI, HTTPException, UploadFile, File +from fastapi import FastAPI, HTTPException, UploadFile, File, Depends, Security +from fastapi.security import APIKeyHeader from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field @@ -35,10 +39,60 @@ version="1.0.0", ) +def is_safe_url(url: str) -> bool: + try: + parsed = urllib.parse.urlparse(url) + if parsed.scheme not in ("http", "https"): + return False + hostname = parsed.hostname + if not hostname: + return False + ip = socket.gethostbyname(hostname) + ip_obj = ipaddress.ip_address(ip) + if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local: + return False + return True + except Exception: + return False + +def sanitize_filename(filename: str) -> str: + basename = os.path.basename(filename) + if not basename or basename in {".", ".."}: + raise ValueError("Invalid filename") + return basename + +# ── Auth setup ─────────────────────────────────────────── +API_KEY_NAME = "Authorization" +api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) + +def get_api_key(api_key: str = Security(api_key_header)) -> str: + # Look for RAG_API_KEY, fallback to None to force config errors when missing rather than failing open + expected_key = os.getenv("RAG_API_KEY") + + if not expected_key: + print("WARNING: RAG_API_KEY is not set in the environment. API is insecure.") + return "" # If no key configured, we let it pass for dev. But ideally we should block. + # To strictly enforce, uncomment the below lines: + # raise HTTPException(status_code=500, detail="Server misconfiguration: RAG_API_KEY not set.") + + if not api_key: + raise HTTPException(status_code=401, detail="Missing Authorization header") + + # strip "Bearer " if provided + token = api_key.replace("Bearer ", "").strip() + + if token != expected_key: + raise HTTPException(status_code=401, detail="Invalid API Key") + return token + app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, + allow_origins=[ + "http://localhost:3000", + "https://flashfetch.vercel.app", + "*" # Wildcard for the Chrome Extension + ], + allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) @@ -101,7 +155,7 @@ async def auto_ingest_on_startup(): # ── Routes ──────────────────────────────────────────────── @app.post("/ask", response_model=AnswerResponse) -def ask(query: Query): +def ask(query: Query, api_key: str = Depends(get_api_key)): """ Accepts a question, retrieves relevant document chunks, and returns a grounded answer from the Groq LLM. @@ -118,7 +172,7 @@ def ask(query: Query): @app.post("/ask-with-context", response_model=AnswerResponse) -def ask_with_context(query: InlineQuery): +def ask_with_context(query: InlineQuery, api_key: str = Depends(get_api_key)): """ Answer a question using raw text passed inline (no document upload needed). The Chrome extension uses this when a file is open in the browser. @@ -137,13 +191,15 @@ def ask_with_context(query: InlineQuery): @app.post("/extract-url") -def extract_url(req: UrlExtractRequest): +def extract_url(req: UrlExtractRequest, api_key: str = Depends(get_api_key)): """ Download any URL and extract its text content. Supports Google Drive share links, direct PDF URLs, plain text URLs. Returns { filename, text, char_count } for use with /ask-with-context. """ url = req.url.strip() + if not is_safe_url(url): + raise HTTPException(status_code=400, detail="Invalid or unsafe URL provided.") # ── Convert Google Drive share/view/preview URL → direct download ───── # Patterns: @@ -244,15 +300,19 @@ def _list_docs() -> list[dict]: @app.get("/documents") -def list_documents(): +def list_documents(api_key: str = Depends(get_api_key)): """Return all documents currently in the docs/ folder.""" return {"documents": _list_docs()} @app.post("/upload") -async def upload_document(file: UploadFile = File(...)): +async def upload_document(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): """Upload a .txt / .md / .pdf file, re-index everything, return updated doc list.""" - fname = file.filename or "" + try: + fname = sanitize_filename(file.filename or "") + except ValueError: + raise HTTPException(status_code=400, detail="Invalid filename.") + if not fname.lower().endswith(SUPPORTED): raise HTTPException( status_code=400, @@ -276,9 +336,14 @@ async def upload_document(file: UploadFile = File(...)): @app.delete("/documents/{filename}") -def delete_document(filename: str): +def delete_document(filename: str, api_key: str = Depends(get_api_key)): """Remove a document and re-index.""" - path = os.path.join(DOCS_DIR, filename) + try: + safe_name = sanitize_filename(filename) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid filename.") + + path = os.path.join(DOCS_DIR, safe_name) if not os.path.exists(path): raise HTTPException(status_code=404, detail="File not found.") os.remove(path) diff --git a/rag-backend/requirements.txt b/rag-backend/requirements.txt index 026f33f..6f486e3 100644 --- a/rag-backend/requirements.txt +++ b/rag-backend/requirements.txt @@ -2,7 +2,7 @@ fastapi==0.115.0 uvicorn==0.30.6 sentence-transformers==3.0.1 faiss-cpu==1.13.2 -PyMuPDF==1.23.8 +PyMuPDF>=1.24.0 groq==0.13.0 httpx==0.27.2 python-dotenv==1.0.1 diff --git a/supabase/admin.sql b/supabase/admin.sql index 9ed7c81..daf002e 100644 --- a/supabase/admin.sql +++ b/supabase/admin.sql @@ -1,161 +1,161 @@ --- ============================================================ --- Admin Portal Tables for FlashFetch --- Run this in your Supabase SQL Editor AFTER chat-history.sql --- ============================================================ - --- 1. Admin users table ─────────────────────────────────────── --- To grant admin: INSERT INTO public.admin_users (user_id) VALUES (''); --- To revoke: DELETE FROM public.admin_users WHERE user_id = ''; -CREATE TABLE IF NOT EXISTS public.admin_users ( - user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, - granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- 2. RLS: Only the row's own user OR existing admins can read -ALTER TABLE public.admin_users ENABLE ROW LEVEL SECURITY; - -DROP POLICY IF EXISTS "admins_read_own" ON public.admin_users; -CREATE POLICY "admins_read_own" ON public.admin_users - FOR SELECT USING (auth.uid() = user_id); - --- 3. Analytics view ────────────────────────────────────────── --- Gives admins a single view to query from the dashboard. -DROP VIEW IF EXISTS public.admin_analytics; -CREATE VIEW public.admin_analytics AS -SELECT - -- total sessions - (SELECT COUNT(*) FROM public.chat_sessions) AS total_sessions, - -- total queries (user messages only) - (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'user') AS total_queries, - -- confidence counts - (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'assistant' AND confidence = 'high') AS high_confidence, - (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'assistant' AND confidence = 'medium') AS medium_confidence, - (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'assistant' AND confidence = 'low') AS low_confidence, - -- distinct users - (SELECT COUNT(DISTINCT user_id) FROM public.chat_sessions) AS total_users; - --- 4. Top documents view ────────────────────────────────────── --- Unnests the sources JSONB array to count per-document hits. -DROP VIEW IF EXISTS public.admin_top_documents; -CREATE VIEW public.admin_top_documents AS -SELECT - src->>'document' AS document_name, - COUNT(*) AS hit_count -FROM public.chat_messages, - jsonb_array_elements(sources) AS src -WHERE role = 'assistant' - AND jsonb_array_length(sources) > 0 -GROUP BY 1 -ORDER BY 2 DESC -LIMIT 10; - --- 5. Daily query volume (last 14 days) ────────────────────── -DROP VIEW IF EXISTS public.admin_daily_queries; -CREATE VIEW public.admin_daily_queries AS -SELECT - DATE(created_at)::TEXT AS day, - COUNT(*) AS query_count -FROM public.chat_messages -WHERE role = 'user' - AND created_at >= NOW() - INTERVAL '14 days' -GROUP BY 1 -ORDER BY 1 ASC; - --- 6. Restrict view access ──────────────────────────────────── --- Views cannot have RLS directly (Supabase shows "UNRESTRICTED"). --- Fix: revoke from anon, allow only authenticated, and wrap each --- in a SECURITY DEFINER function that checks admin status. - --- Helper: is current user an admin? -CREATE OR REPLACE FUNCTION public.is_admin() -RETURNS BOOLEAN -LANGUAGE sql SECURITY DEFINER STABLE -AS $$ - SELECT EXISTS ( - SELECT 1 FROM public.admin_users WHERE user_id = auth.uid() + -- ============================================================ + -- Admin Portal Tables for FlashFetch + -- Run this in your Supabase SQL Editor AFTER chat-history.sql + -- ============================================================ + + -- 1. Admin users table ─────────────────────────────────────── + -- To grant admin: INSERT INTO public.admin_users (user_id) VALUES (''); + -- To revoke: DELETE FROM public.admin_users WHERE user_id = ''; + CREATE TABLE IF NOT EXISTS public.admin_users ( + user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -$$; - --- Revoke direct view access from anonymous and public -REVOKE ALL ON public.admin_analytics FROM anon, public; -REVOKE ALL ON public.admin_top_documents FROM anon, public; -REVOKE ALL ON public.admin_daily_queries FROM anon, public; - --- Allow authenticated role to read (app enforces admin check via is_admin()) -GRANT SELECT ON public.admin_analytics TO authenticated; -GRANT SELECT ON public.admin_top_documents TO authenticated; -GRANT SELECT ON public.admin_daily_queries TO authenticated; - --- 7. Secure wrapper functions (SECURITY DEFINER = run as owner) ── --- These enforce the admin check at the database level. --- Call these instead of querying views directly in production. - -CREATE OR REPLACE FUNCTION public.get_admin_analytics() -RETURNS SETOF public.admin_analytics -LANGUAGE sql SECURITY DEFINER STABLE -AS $$ - SELECT * FROM public.admin_analytics - WHERE public.is_admin(); -$$; - -CREATE OR REPLACE FUNCTION public.get_admin_top_documents() -RETURNS SETOF public.admin_top_documents -LANGUAGE sql SECURITY DEFINER STABLE -AS $$ - SELECT * FROM public.admin_top_documents - WHERE public.is_admin(); -$$; - -CREATE OR REPLACE FUNCTION public.get_admin_daily_queries() -RETURNS SETOF public.admin_daily_queries -LANGUAGE sql SECURITY DEFINER STABLE -AS $$ - SELECT * FROM public.admin_daily_queries - WHERE public.is_admin(); -$$; - -GRANT EXECUTE ON FUNCTION public.get_admin_analytics() TO authenticated; -GRANT EXECUTE ON FUNCTION public.get_admin_top_documents() TO authenticated; -GRANT EXECUTE ON FUNCTION public.get_admin_daily_queries() TO authenticated; - --- 8. Allow admins to SELECT all rows in chat tables ────────── --- Without these policies the views only see the calling user's rows. --- With SECURITY DEFINER the functions run as postgres (bypasses RLS), --- but direct .from() calls need explicit admin-read policies. - -DROP POLICY IF EXISTS "admins_read_all_sessions" ON public.chat_sessions; -CREATE POLICY "admins_read_all_sessions" ON public.chat_sessions - FOR SELECT USING (public.is_admin()); - -DROP POLICY IF EXISTS "admins_read_all_messages" ON public.chat_messages; -CREATE POLICY "admins_read_all_messages" ON public.chat_messages - FOR SELECT USING (public.is_admin()); - --- 9. get_all_sessions RPC ────────────────────────────────────── --- Returns all sessions across all users for the admin dashboard. --- SECURITY DEFINER ensures RLS is bypassed (runs as postgres). -CREATE OR REPLACE FUNCTION public.get_all_sessions(row_limit INT DEFAULT 20) -RETURNS TABLE ( - id UUID, - user_id UUID, - title TEXT, - created_at TIMESTAMPTZ, - message_count BIGINT -) -LANGUAGE sql SECURITY DEFINER STABLE -AS $$ + + -- 2. RLS: Only the row's own user OR existing admins can read + ALTER TABLE public.admin_users ENABLE ROW LEVEL SECURITY; + + DROP POLICY IF EXISTS "admins_read_own" ON public.admin_users; + CREATE POLICY "admins_read_own" ON public.admin_users + FOR SELECT USING (auth.uid() = user_id); + + -- 3. Analytics view ────────────────────────────────────────── + -- Gives admins a single view to query from the dashboard. + DROP VIEW IF EXISTS public.admin_analytics; + CREATE VIEW public.admin_analytics AS + SELECT + -- total sessions + (SELECT COUNT(*) FROM public.chat_sessions) AS total_sessions, + -- total queries (user messages only) + (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'user') AS total_queries, + -- confidence counts + (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'assistant' AND confidence = 'high') AS high_confidence, + (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'assistant' AND confidence = 'medium') AS medium_confidence, + (SELECT COUNT(*) FROM public.chat_messages WHERE role = 'assistant' AND confidence = 'low') AS low_confidence, + -- distinct users + (SELECT COUNT(DISTINCT user_id) FROM public.chat_sessions) AS total_users; + + -- 4. Top documents view ────────────────────────────────────── + -- Unnests the sources JSONB array to count per-document hits. + DROP VIEW IF EXISTS public.admin_top_documents; + CREATE VIEW public.admin_top_documents AS + SELECT + src->>'document' AS document_name, + COUNT(*) AS hit_count + FROM public.chat_messages, + jsonb_array_elements(sources) AS src + WHERE role = 'assistant' + AND jsonb_array_length(sources) > 0 + GROUP BY 1 + ORDER BY 2 DESC + LIMIT 10; + + -- 5. Daily query volume (last 14 days) ────────────────────── + DROP VIEW IF EXISTS public.admin_daily_queries; + CREATE VIEW public.admin_daily_queries AS SELECT - s.id, - s.user_id, - s.title, - s.created_at, - COUNT(m.id) AS message_count - FROM public.chat_sessions s - LEFT JOIN public.chat_messages m ON m.session_id = s.id - WHERE public.is_admin() - GROUP BY s.id, s.user_id, s.title, s.created_at - ORDER BY s.created_at DESC - LIMIT row_limit; -$$; - -GRANT EXECUTE ON FUNCTION public.get_all_sessions(INT) TO authenticated; + DATE(created_at)::TEXT AS day, + COUNT(*) AS query_count + FROM public.chat_messages + WHERE role = 'user' + AND created_at >= NOW() - INTERVAL '14 days' + GROUP BY 1 + ORDER BY 1 ASC; + + -- 6. Restrict view access ──────────────────────────────────── + -- Views cannot have RLS directly (Supabase shows "UNRESTRICTED"). + -- Fix: revoke from anon, allow only authenticated, and wrap each + -- in a SECURITY DEFINER function that checks admin status. + + -- Helper: is current user an admin? + CREATE OR REPLACE FUNCTION public.is_admin() + RETURNS BOOLEAN + LANGUAGE sql SECURITY DEFINER STABLE + AS $$ + SELECT EXISTS ( + SELECT 1 FROM public.admin_users WHERE user_id = auth.uid() + ); + $$; + + -- Revoke direct view access from anonymous and public + REVOKE ALL ON public.admin_analytics FROM anon, public; + REVOKE ALL ON public.admin_top_documents FROM anon, public; + REVOKE ALL ON public.admin_daily_queries FROM anon, public; + + -- Allow authenticated role to read (app enforces admin check via is_admin()) + GRANT SELECT ON public.admin_analytics TO authenticated; + GRANT SELECT ON public.admin_top_documents TO authenticated; + GRANT SELECT ON public.admin_daily_queries TO authenticated; + + -- 7. Secure wrapper functions (SECURITY DEFINER = run as owner) ── + -- These enforce the admin check at the database level. + -- Call these instead of querying views directly in production. + + CREATE OR REPLACE FUNCTION public.get_admin_analytics() + RETURNS SETOF public.admin_analytics + LANGUAGE sql SECURITY DEFINER STABLE + AS $$ + SELECT * FROM public.admin_analytics + WHERE public.is_admin(); + $$; + + CREATE OR REPLACE FUNCTION public.get_admin_top_documents() + RETURNS SETOF public.admin_top_documents + LANGUAGE sql SECURITY DEFINER STABLE + AS $$ + SELECT * FROM public.admin_top_documents + WHERE public.is_admin(); + $$; + + CREATE OR REPLACE FUNCTION public.get_admin_daily_queries() + RETURNS SETOF public.admin_daily_queries + LANGUAGE sql SECURITY DEFINER STABLE + AS $$ + SELECT * FROM public.admin_daily_queries + WHERE public.is_admin(); + $$; + + GRANT EXECUTE ON FUNCTION public.get_admin_analytics() TO authenticated; + GRANT EXECUTE ON FUNCTION public.get_admin_top_documents() TO authenticated; + GRANT EXECUTE ON FUNCTION public.get_admin_daily_queries() TO authenticated; + + -- 8. Allow admins to SELECT all rows in chat tables ────────── + -- Without these policies the views only see the calling user's rows. + -- With SECURITY DEFINER the functions run as postgres (bypasses RLS), + -- but direct .from() calls need explicit admin-read policies. + + DROP POLICY IF EXISTS "admins_read_all_sessions" ON public.chat_sessions; + CREATE POLICY "admins_read_all_sessions" ON public.chat_sessions + FOR SELECT USING (public.is_admin()); + + DROP POLICY IF EXISTS "admins_read_all_messages" ON public.chat_messages; + CREATE POLICY "admins_read_all_messages" ON public.chat_messages + FOR SELECT USING (public.is_admin()); + + -- 9. get_all_sessions RPC ────────────────────────────────────── + -- Returns all sessions across all users for the admin dashboard. + -- SECURITY DEFINER ensures RLS is bypassed (runs as postgres). + CREATE OR REPLACE FUNCTION public.get_all_sessions(row_limit INT DEFAULT 20) + RETURNS TABLE ( + id UUID, + user_id UUID, + title TEXT, + created_at TIMESTAMPTZ, + message_count BIGINT + ) + LANGUAGE sql SECURITY DEFINER STABLE + AS $$ + SELECT + s.id, + s.user_id, + s.title, + s.created_at, + COUNT(m.id) AS message_count + FROM public.chat_sessions s + LEFT JOIN public.chat_messages m ON m.session_id = s.id + WHERE public.is_admin() + GROUP BY s.id, s.user_id, s.title, s.created_at + ORDER BY s.created_at DESC + LIMIT row_limit; + $$; + + GRANT EXECUTE ON FUNCTION public.get_all_sessions(INT) TO authenticated; From d15b3f5f3e190449cadc2938a713d2ad11ea2c1a Mon Sep 17 00:00:00 2001 From: "PRODHOSH V.S" Date: Mon, 9 Mar 2026 20:45:06 +0530 Subject: [PATCH 3/4] Delete rag-backend/docs/codes.txt --- rag-backend/docs/codes.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rag-backend/docs/codes.txt diff --git a/rag-backend/docs/codes.txt b/rag-backend/docs/codes.txt deleted file mode 100644 index 150008e..0000000 --- a/rag-backend/docs/codes.txt +++ /dev/null @@ -1 +0,0 @@ -hi ra puka how are you \ No newline at end of file From 88554cc3f9483110692be49408f54f7381f364c2 Mon Sep 17 00:00:00 2001 From: saireddy Date: Mon, 9 Mar 2026 20:54:23 +0530 Subject: [PATCH 4/4] resloved the comments --- app/api/rag/[...path]/route.ts | 24 +++++++++++++++----- flashfetch-extension/popup.js | 2 +- rag-backend/main.py | 41 ++++++++++++++++++++++++---------- rag-backend/requirements.txt | 2 +- supabase/admin.sql | 16 +++++-------- 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/app/api/rag/[...path]/route.ts b/app/api/rag/[...path]/route.ts index 9e76744..e1a6659 100644 --- a/app/api/rag/[...path]/route.ts +++ b/app/api/rag/[...path]/route.ts @@ -2,7 +2,9 @@ import { NextRequest, NextResponse } from "next/server"; import { CookieOptions, createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; -const RAG_API_URL = process.env.NEXT_PUBLIC_RAG_API_URL || "http://127.0.0.1:8000"; +// Use a server-only env var (not NEXT_PUBLIC_*) to avoid leaking the internal +// backend URL into the client bundle. +const RAG_API_URL = process.env.RAG_API_URL || process.env.NEXT_PUBLIC_RAG_API_URL || "http://127.0.0.1:8000"; const RAG_API_KEY = process.env.RAG_API_KEY || ""; export async function GET(request: NextRequest) { @@ -18,6 +20,15 @@ export async function DELETE(request: NextRequest) { } async function proxyRequest(request: NextRequest) { + // 0. Fail fast if RAG_API_KEY is not configured + if (!RAG_API_KEY) { + console.error("[Proxy Error]: RAG_API_KEY environment variable is not set."); + return NextResponse.json( + { detail: "Server misconfiguration: backend API key not set." }, + { status: 500 } + ); + } + // 1. Verify Authentication const cookieStore = await cookies(); const supabase = createServerClient( @@ -69,15 +80,18 @@ async function proxyRequest(request: NextRequest) { headers.set("Content-Type", contentType); } - // 4. Forward the request + // 4. Forward the request — stream the body instead of buffering try { const fetchOptions: RequestInit = { method: request.method, headers: headers, - // For GET/HEAD requests, body cannot be included - body: ["GET", "HEAD"].includes(request.method) ? undefined : await request.blob(), + // Stream the body directly instead of buffering via .blob() + // For GET/HEAD requests, body must be omitted. + body: ["GET", "HEAD"].includes(request.method) ? undefined : request.body, // Disable caching for proxy requests cache: "no-store", + // @ts-expect-error -- Next.js extended fetch supports duplex for streaming + duplex: "half", }; const response = await fetch(backendUrl, fetchOptions); @@ -93,7 +107,7 @@ async function proxyRequest(request: NextRequest) { statusText: response.statusText, headers: responseHeaders, }); - } catch (error: any) { + } catch (error: unknown) { console.error("[Proxy Error]:", error); return NextResponse.json( { detail: "Backend connection failed" }, diff --git a/flashfetch-extension/popup.js b/flashfetch-extension/popup.js index 0568bf9..3156f9e 100644 --- a/flashfetch-extension/popup.js +++ b/flashfetch-extension/popup.js @@ -199,7 +199,7 @@ saveTokenBtn.addEventListener("click", () => { }); logoutBtn.addEventListener("click", () => { - chrome.storage.local.remove(["ff_api_url"], () => { + chrome.storage.local.remove(["ff_api_url", "ff_api_key"], () => { conversationHistory = []; activeFileContext = null; setStatus("Reset done", "success"); diff --git a/rag-backend/main.py b/rag-backend/main.py index 4d83934..96bad07 100644 --- a/rag-backend/main.py +++ b/rag-backend/main.py @@ -39,7 +39,16 @@ version="1.0.0", ) +# Allowed redirect hosts (we only follow redirects from these domains) +_REDIRECT_ALLOW = {"drive.google.com", "docs.google.com", "googleusercontent.com"} + def is_safe_url(url: str) -> bool: + """Validate that *url* resolves exclusively to global (public) IPs. + + Uses getaddrinfo to check **all** A/AAAA records (not just the first), + and enforces ip_address.is_global so link-local, loopback, private and + reserved ranges are all rejected. + """ try: parsed = urllib.parse.urlparse(url) if parsed.scheme not in ("http", "https"): @@ -47,10 +56,14 @@ def is_safe_url(url: str) -> bool: hostname = parsed.hostname if not hostname: return False - ip = socket.gethostbyname(hostname) - ip_obj = ipaddress.ip_address(ip) - if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local: + # Resolve every A and AAAA record for the hostname + addrs = socket.getaddrinfo(hostname, None, proto=socket.IPPROTO_TCP) + if not addrs: return False + for family, _type, _proto, _canonname, sockaddr in addrs: + ip_obj = ipaddress.ip_address(sockaddr[0]) + if not ip_obj.is_global: + return False return True except Exception: return False @@ -66,21 +79,25 @@ def sanitize_filename(filename: str) -> str: api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) def get_api_key(api_key: str = Security(api_key_header)) -> str: - # Look for RAG_API_KEY, fallback to None to force config errors when missing rather than failing open + """Validate the Authorization header against RAG_API_KEY. + + Fail-closed: if the env var is unset the server returns 500 instead of + silently allowing unauthenticated access. + """ expected_key = os.getenv("RAG_API_KEY") - + if not expected_key: - print("WARNING: RAG_API_KEY is not set in the environment. API is insecure.") - return "" # If no key configured, we let it pass for dev. But ideally we should block. - # To strictly enforce, uncomment the below lines: - # raise HTTPException(status_code=500, detail="Server misconfiguration: RAG_API_KEY not set.") + raise HTTPException( + status_code=500, + detail="Server misconfiguration: RAG_API_KEY is not set.", + ) if not api_key: raise HTTPException(status_code=401, detail="Missing Authorization header") - - # strip "Bearer " if provided + + # strip "Bearer " prefix if provided token = api_key.replace("Bearer ", "").strip() - + if token != expected_key: raise HTTPException(status_code=401, detail="Invalid API Key") return token diff --git a/rag-backend/requirements.txt b/rag-backend/requirements.txt index 6f486e3..e86c077 100644 --- a/rag-backend/requirements.txt +++ b/rag-backend/requirements.txt @@ -2,7 +2,7 @@ fastapi==0.115.0 uvicorn==0.30.6 sentence-transformers==3.0.1 faiss-cpu==1.13.2 -PyMuPDF>=1.24.0 +PyMuPDF>=1.24.0,<2 groq==0.13.0 httpx==0.27.2 python-dotenv==1.0.1 diff --git a/supabase/admin.sql b/supabase/admin.sql index daf002e..7d0afbc 100644 --- a/supabase/admin.sql +++ b/supabase/admin.sql @@ -11,7 +11,7 @@ granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); - -- 2. RLS: Only the row's own user OR existing admins can read + -- 2. RLS: Only the row's own user can read their admin_users entry ALTER TABLE public.admin_users ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admins_read_own" ON public.admin_users; @@ -76,15 +76,11 @@ ); $$; - -- Revoke direct view access from anonymous and public - REVOKE ALL ON public.admin_analytics FROM anon, public; - REVOKE ALL ON public.admin_top_documents FROM anon, public; - REVOKE ALL ON public.admin_daily_queries FROM anon, public; - - -- Allow authenticated role to read (app enforces admin check via is_admin()) - GRANT SELECT ON public.admin_analytics TO authenticated; - GRANT SELECT ON public.admin_top_documents TO authenticated; - GRANT SELECT ON public.admin_daily_queries TO authenticated; + -- Revoke direct view access from all roles. + -- Access is only permitted through the admin-only SECURITY DEFINER RPCs below. + REVOKE ALL ON public.admin_analytics FROM anon, public, authenticated; + REVOKE ALL ON public.admin_top_documents FROM anon, public, authenticated; + REVOKE ALL ON public.admin_daily_queries FROM anon, public, authenticated; -- 7. Secure wrapper functions (SECURITY DEFINER = run as owner) ── -- These enforce the admin check at the database level.