Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): backend authentification verification #144

Merged
merged 9 commits into from
May 24, 2023
Merged
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
4 changes: 3 additions & 1 deletion .backend_env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
SUPABASE_URL="XXXXX"
SUPABASE_SERVICE_KEY="eyXXXXX"
OPENAI_API_KEY="sk-XXXXXX"
ANTHROPIC_API_KEY="XXXXXX"
ANTHROPIC_API_KEY="XXXXXX"
JWT_SECRET_KEY="Found in Supabase settings in the API tab"
AUTHENTICATE="true"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ streamlit-demo/.streamlit/secrets.toml
.backend_env
.frontend_env
backend/pandoc-*
**/yarn.lock
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ cp .frontend_env.example frontend/.env
- **Step 3**: Update the `backend/.env` and `frontend/.env` file

> _Your `supabase_service_key` can be found in your Supabase dashboard under Project Settings -> API. Use the `anon` `public` key found in the `Project API keys` section._
> _Your `JWT_SECRET_KEY`can be found in your supabase settings under Project Settings -> JWT Settings -> JWT Secret_

- **Step 4**: Run the following migration scripts on the Supabase database via the web interface (SQL Editor -> `New query`)

Expand Down
20 changes: 12 additions & 8 deletions backend/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from fastapi.middleware.cors import CORSMiddleware
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi import FastAPI, UploadFile, File, HTTPException, Header, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import os
from pydantic import BaseModel
from typing import List, Tuple
from typing import List, Tuple, Annotated
from supabase import create_client, Client
from tempfile import SpooledTemporaryFile
from auth_bearer import JWTBearer
import shutil
import pypandoc

Expand Down Expand Up @@ -87,13 +89,15 @@ async def filter_file(file: UploadFile, enable_summarization: bool, supabase_cli
return {"message": f"❌ {file.filename} is not supported.", "type": "error"}


@app.post("/upload")

@app.post("/upload", dependencies=[Depends(JWTBearer())])
async def upload_file(commons: CommonsDep, file: UploadFile, enable_summarization: bool = False):
message = await filter_file(file, enable_summarization, commons['supabase'])

return message


@app.post("/chat/")
@app.post("/chat/", dependencies=[Depends(JWTBearer())])
async def chat_endpoint(commons: CommonsDep, chat_message: ChatMessage):
history = chat_message.history
qa = get_qa_llm(chat_message)
Expand Down Expand Up @@ -124,7 +128,7 @@ async def chat_endpoint(commons: CommonsDep, chat_message: ChatMessage):
return {"history": history}


@app.post("/crawl/")
@app.post("/crawl/", dependencies=[Depends(JWTBearer())])
async def crawl_endpoint(commons: CommonsDep, crawl_website: CrawlWebsite, enable_summarization: bool = False):
file_path, file_name = crawl_website.process()

Expand All @@ -139,7 +143,7 @@ async def crawl_endpoint(commons: CommonsDep, crawl_website: CrawlWebsite, enabl
return message


@app.get("/explore")
@app.get("/explore", dependencies=[Depends(JWTBearer())])
async def explore_endpoint(commons: CommonsDep):
response = commons['supabase'].table("documents").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact").execute()
Expand All @@ -152,7 +156,7 @@ async def explore_endpoint(commons: CommonsDep):
return {"documents": unique_data}


@app.delete("/explore/{file_name}")
@app.delete("/explore/{file_name}", dependencies=[Depends(JWTBearer())])
async def delete_endpoint(commons: CommonsDep, file_name: str):
# Cascade delete the summary from the database first, because it has a foreign key constraint
commons['supabase'].table("summaries").delete().match(
Expand All @@ -162,7 +166,7 @@ async def delete_endpoint(commons: CommonsDep, file_name: str):
return {"message": f"{file_name} has been deleted."}


@app.get("/explore/{file_name}")
@app.get("/explore/{file_name}", dependencies=[Depends(JWTBearer())])
async def download_endpoint(commons: CommonsDep, file_name: str):
response = commons['supabase'].table("documents").select(
"metadata->>file_name, metadata->>file_size, metadata->>file_extension, metadata->>file_url").match({"metadata->>file_name": file_name}).execute()
Expand Down
31 changes: 31 additions & 0 deletions backend/auth_bearer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from fastapi import Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional
import os

from auth_handler import decode_access_token

class JWTBearer(HTTPBearer):
def __init__(self, auto_error: bool = True):
super().__init__(auto_error=auto_error)

async def __call__(self, request: Request):
credentials: Optional[HTTPAuthorizationCredentials] = await super().__call__(request)
if os.environ.get("AUTHENTICATE") == "false":
return True
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(status_code=402, detail="Invalid authorization scheme.")
token = credentials.credentials
if not self.verify_jwt(token):
raise HTTPException(status_code=402, detail="Invalid token or expired token.")
return credentials.credentials
else:
raise HTTPException(status_code=403, detail="Invalid authorization code.")

def verify_jwt(self, jwtoken: str) -> bool:
isTokenValid: bool = False
payload = decode_access_token(jwtoken)
if payload:
isTokenValid = True
return isTokenValid
26 changes: 26 additions & 0 deletions backend/auth_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from jose import jwt
from typing import Optional
from datetime import datetime, timedelta
from jose.exceptions import JWTError
import os

SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
ALGORITHM = "HS256"

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

def decode_access_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_aud": False})
return payload
except JWTError as e:
print(f"JWTError: {str(e)}")
return None
2 changes: 0 additions & 2 deletions backend/parsers/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ async def process_audio(upload_file: UploadFile, stats_db):

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=chunk_size, chunk_overlap=chunk_overlap)
print(transcript)
print("#########")
texts = text_splitter.split_text(transcript)

docs_with_metadata = [Document(page_content=text, metadata={"file_sha1": file_sha, "file_size": file_size, "file_name": file_meta_name,
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ uvicorn==0.22.0
pypandoc==1.11
docx2txt==0.8
guidance==0.0.53
python-jose==3.3.0
23 changes: 12 additions & 11 deletions frontend/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,20 @@ import Modal from "../components/ui/Modal";
import { MdSettings } from "react-icons/md";
import ChatMessages from "./ChatMessages";
import PageHeading from "../components/ui/PageHeading";
import { useSupabase } from "../supabase-provider";
import { redirect } from "next/navigation";

export default function ChatPage() {
const [question, setQuestion] = useState("");
const [history, setHistory] = useState<Array<[string, string]>>([
// ["user", "Hello!"],
// ["assistant", "Hello Back!"],
// ["user", "Send some long message"],
// [
// "assistant",
// "This is a very long and really long message which is longer than every other message.",
// ],
// ["user", "What is redux"],
// ["assistant", ``],
]);
const [history, setHistory] = useState<Array<[string, string]>>([]);
const [model, setModel] = useState("gpt-3.5-turbo");
const [temperature, setTemperature] = useState(0);
const [maxTokens, setMaxTokens] = useState(500);
const [isPending, setIsPending] = useState(false);
const { supabase, session } = useSupabase()
if (session === null) {
redirect('/login')
}

const askQuestion = async () => {
setHistory((hist) => [...hist, ["user", question]]);
Expand All @@ -37,6 +33,11 @@ export default function ChatPage() {
history,
temperature,
max_tokens: maxTokens,
},
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
}
);
setHistory(response.data.history);
Expand Down
13 changes: 12 additions & 1 deletion frontend/app/explore/DocumentItem/DocumentData.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import axios from "axios";
import { Document } from "../types";
import { useSupabase } from "../../supabase-provider";

interface DocumentDataProps {
documentName: string;
}

const DocumentData = async ({ documentName }: DocumentDataProps) => {
const { supabase, session } = useSupabase();
if (!session) {
throw new Error('User session not found');
}

const res = await axios.get(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${documentName}`
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${documentName}`,
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
}
);
const documents = res.data.documents as any[];
const doc = documents[0];
Expand Down
13 changes: 12 additions & 1 deletion frontend/app/explore/DocumentItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Dispatch, SetStateAction, Suspense, useState } from "react";
import axios from "axios";
import DocumentData from "./DocumentData";
import Spinner from "@/app/components/ui/Spinner";
import { useSupabase } from "@/app/supabase-provider";

interface DocumentProps {
document: Document;
Expand All @@ -15,13 +16,23 @@ interface DocumentProps {

const DocumentItem = ({ document, setDocuments }: DocumentProps) => {
const [isDeleting, setIsDeleting] = useState(false);
const { supabase, session } = useSupabase()
if (!session) {
throw new Error('User session not found');
}


const deleteDocument = async (name: string) => {
setIsDeleting(true);
try {
console.log(`Deleting Document ${name}`);
const response = await axios.delete(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${name}`
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${name}`,
{
headers: {
Authorization: `Bearer ${session.access_token}`,
},
}
);
setDocuments((docs) => docs.filter((doc) => doc.name !== name)); // Optimistic update
} catch (error) {
Expand Down
15 changes: 14 additions & 1 deletion frontend/app/explore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import Button from "../components/ui/Button";
import Link from "next/link";
import Spinner from "../components/ui/Spinner";
import { AnimatePresence } from "framer-motion";
import { useSupabase } from "../supabase-provider";
import { redirect } from "next/navigation";



export default function ExplorePage() {
const [documents, setDocuments] = useState<Document[]>([]);
const [isPending, setIsPending] = useState(true);
const { supabase, session } = useSupabase();
if (session === null) {
redirect('/login')
}

useEffect(() => {
fetchDocuments();
Expand All @@ -23,7 +31,12 @@ export default function ExplorePage() {
`Fetching documents from ${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`
);
const response = await axios.get<{ documents: Document[] }>(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`,
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
}
);
setDocuments(response.data.documents);
} catch (error) {
Expand Down
Binary file modified frontend/app/favicon.ico
Binary file not shown.
6 changes: 6 additions & 0 deletions frontend/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import Card from "../components/ui/Card";
import Button from "../components/ui/Button";
import PageHeading from "../components/ui/PageHeading";
import { useSupabase } from "../supabase-provider";
import { redirect } from "next/navigation";
import Link from "next/link";


export default function Login() {
const { supabase } = useSupabase();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");


const handleLogin = async () => {
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
Expand Down Expand Up @@ -47,7 +51,9 @@ export default function Login() {
/>
<div className="flex justify-center gap-3">
<Button onClick={handleLogin}>Login</Button>
<Link href="/signup">Dont have an account? Sign up</Link>
</div>

</div>
</Card>
</section>
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/supabase-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ export const useSupabase = () => {
}

return context
}
}
20 changes: 18 additions & 2 deletions frontend/app/upload/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import Card from "../components/ui/Card";
import PageHeading from "../components/ui/PageHeading";
import { useSupabase } from "../supabase-provider";
import { redirect } from "next/navigation";

export default function UploadPage() {
const [message, setMessage] = useState<Message | null>(null);
const [isPending, setIsPending] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
const urlInputRef = useRef<HTMLInputElement | null>(null);
const { supabase, session } = useSupabase()
if (session === null) {
redirect('/login')
}

const crawlWebsite = useCallback(async () => {
// Validate URL
Expand All @@ -41,7 +47,12 @@ export default function UploadPage() {
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/crawl`,
config
config,
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
}
);

setMessage({
Expand All @@ -62,7 +73,12 @@ export default function UploadPage() {
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/upload`,
formData
formData,
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
}
);

setMessage({
Expand Down