feat(auth): now application has authentication (#144)

* feat(auth): backend authentification verification

* feat(auth): added to all endpoints

* feat(auth): added to all endpoints

* feat(auth): redirect if not connected

* chore(print): removed

* feat(login): redirect

* feat(icon): added

* chore(yarn): removed lock

* chore(gitignore): removed
This commit is contained in:
Stan Girard 2023-05-24 22:21:22 +02:00 committed by GitHub
parent 9aa16fe6fc
commit 327074c5d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 150 additions and 28 deletions

View File

@ -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
.gitignore vendored
View File

@ -49,3 +49,4 @@ streamlit-demo/.streamlit/secrets.toml
.backend_env
.frontend_env
backend/pandoc-*
**/yarn.lock

View File

@ -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`)

View File

@ -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
@ -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)
@ -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()
@ -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()
@ -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(
@ -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()

31
backend/auth_bearer.py Normal file
View File

@ -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
backend/auth_handler.py Normal file
View File

@ -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

View File

@ -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,

View File

@ -15,3 +15,4 @@ uvicorn==0.22.0
pypandoc==1.11
docx2txt==0.8
guidance==0.0.53
python-jose==3.3.0

View File

@ -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]]);
@ -37,6 +33,11 @@ export default function ChatPage() {
history,
temperature,
max_tokens: maxTokens,
},
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
}
);
setHistory(response.data.history);

View File

@ -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];

View File

@ -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;
@ -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) {

View File

@ -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();
@ -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) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -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,
@ -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>

View File

@ -53,4 +53,4 @@ export const useSupabase = () => {
}
return context
}
}

View File

@ -9,6 +9,8 @@ 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);
@ -16,6 +18,10 @@ export default function UploadPage() {
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
@ -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({
@ -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({