dl4ds_tutor / code /app.py
XThomasBU
minor fix
66d8970
raw
history blame
11.2 kB
from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests
from google_auth_oauthlib.flow import Flow
from chainlit.utils import mount_chainlit
import secrets
import json
import base64
from modules.config.constants import (
OAUTH_GOOGLE_CLIENT_ID,
OAUTH_GOOGLE_CLIENT_SECRET,
CHAINLIT_URL,
GITHUB_REPO,
DOCS_WEBSITE,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from modules.chat_processor.helpers import (
get_user_details,
get_time,
reset_tokens_for_user,
check_user_cooldown,
update_user_info,
)
GOOGLE_CLIENT_ID = OAUTH_GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET = OAUTH_GOOGLE_CLIENT_SECRET
GOOGLE_REDIRECT_URI = f"{CHAINLIT_URL}/auth/oauth/google/callback"
app = FastAPI()
app.mount("/public", StaticFiles(directory="public"), name="public")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Update with appropriate origins
allow_methods=["*"],
allow_headers=["*"], # or specify the headers you want to allow
expose_headers=["X-User-Info"], # Expose the custom header
)
templates = Jinja2Templates(directory="templates")
session_store = {}
CHAINLIT_PATH = "/chainlit_tutor"
# only admin is given any additional permissions for now -- no limits on tokens
USER_ROLES = {
"tgardos@bu.edu": ["instructor", "bu"],
"xthomas@bu.edu": ["admin", "instructor", "bu"],
"faridkar@bu.edu": ["instructor", "bu"],
"xavierohan1@gmail.com": ["guest"],
# Add more users and roles as needed
}
# Create a Google OAuth flow
flow = Flow.from_client_config(
{
"web": {
"client_id": GOOGLE_CLIENT_ID,
"client_secret": GOOGLE_CLIENT_SECRET,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": [GOOGLE_REDIRECT_URI],
"scopes": [
"openid",
# "https://www.googleapis.com/auth/userinfo.email",
# "https://www.googleapis.com/auth/userinfo.profile",
],
}
},
scopes=[
"openid",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
],
redirect_uri=GOOGLE_REDIRECT_URI,
)
def get_user_role(username: str):
return USER_ROLES.get(username, ["student"]) # Default to "student" role
async def get_user_info_from_cookie(request: Request):
user_info_encoded = request.cookies.get("X-User-Info")
if user_info_encoded:
try:
user_info_json = base64.b64decode(user_info_encoded).decode()
return json.loads(user_info_json)
except Exception as e:
print(f"Error decoding user info: {e}")
return None
return None
async def del_user_info_from_cookie(request: Request, response: Response):
response.delete_cookie("X-User-Info")
response.delete_cookie("session_token")
session_token = request.cookies.get("session_token")
if session_token:
del session_store[session_token]
def get_user_info(request: Request):
session_token = request.cookies.get("session_token")
if session_token and session_token in session_store:
return session_store[session_token]
return None
@app.get("/", response_class=HTMLResponse)
async def login_page(request: Request):
user_info = await get_user_info_from_cookie(request)
if user_info and user_info.get("google_signed_in"):
return RedirectResponse("/post-signin")
return templates.TemplateResponse(
"login.html",
{"request": request, "GITHUB_REPO": GITHUB_REPO, "DOCS_WEBSITE": DOCS_WEBSITE},
)
# @app.get("/login/guest")
# async def login_guest():
# username = "guest"
# session_token = secrets.token_hex(16)
# unique_session_id = secrets.token_hex(8)
# username = f"{username}_{unique_session_id}"
# session_store[session_token] = {
# "email": username,
# "name": "Guest",
# "profile_image": "",
# "google_signed_in": False, # Ensure guest users do not have this flag
# }
# user_info_json = json.dumps(session_store[session_token])
# user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
# # Set cookies
# response = RedirectResponse(url="/post-signin", status_code=303)
# response.set_cookie(key="session_token", value=session_token)
# response.set_cookie(key="X-User-Info", value=user_info_encoded, httponly=True)
# return response
@app.get("/login/google")
async def login_google(request: Request):
# Clear any existing session cookies to avoid conflicts with guest sessions
response = RedirectResponse(url="/post-signin")
response.delete_cookie(key="session_token")
response.delete_cookie(key="X-User-Info")
user_info = await get_user_info_from_cookie(request)
# Check if user is already signed in using Google
if user_info and user_info.get("google_signed_in"):
return RedirectResponse("/post-signin")
else:
authorization_url, _ = flow.authorization_url(prompt="consent")
return RedirectResponse(authorization_url, headers=response.headers)
@app.get("/auth/oauth/google/callback")
async def auth_google(request: Request):
try:
flow.fetch_token(code=request.query_params.get("code"))
credentials = flow.credentials
user_info = id_token.verify_oauth2_token(
credentials.id_token, google_requests.Request(), GOOGLE_CLIENT_ID
)
email = user_info["email"]
name = user_info.get("name", "")
profile_image = user_info.get("picture", "")
role = get_user_role(email)
session_token = secrets.token_hex(16)
session_store[session_token] = {
"email": email,
"name": name,
"profile_image": profile_image,
"google_signed_in": True, # Set this flag to True for Google-signed users
}
# add literalai user info to session store to be sent to chainlit
literalai_user = await get_user_details(email)
session_store[session_token]["literalai_info"] = literalai_user.to_dict()
session_store[session_token]["literalai_info"]["metadata"]["role"] = role
user_info_json = json.dumps(session_store[session_token])
user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
# Set cookies
response = RedirectResponse(url="/post-signin", status_code=303)
response.set_cookie(key="session_token", value=session_token)
response.set_cookie(
key="X-User-Info", value=user_info_encoded
) # TODO: is the flag httponly=True necessary?
return response
except Exception as e:
print(f"Error during Google OAuth callback: {e}")
return RedirectResponse(url="/", status_code=302)
@app.get("/cooldown")
async def cooldown(request: Request):
user_info = await get_user_info_from_cookie(request)
user_details = await get_user_details(user_info["email"])
current_datetime = get_time()
cooldown, cooldown_end_time = check_user_cooldown(user_details, current_datetime)
print(f"User in cooldown: {cooldown}")
print(f"Cooldown end time: {cooldown_end_time}")
if cooldown and "admin" not in get_user_role(user_info["email"]):
return templates.TemplateResponse(
"cooldown.html",
{
"request": request,
"username": user_info["email"],
"role": get_user_role(user_info["email"]),
"cooldown_end_time": cooldown_end_time,
},
)
else:
await update_user_info(user_details)
await reset_tokens_for_user(user_details)
return RedirectResponse("/post-signin")
@app.get("/post-signin", response_class=HTMLResponse)
async def post_signin(request: Request):
user_info = await get_user_info_from_cookie(request)
if not user_info:
user_info = get_user_info(request)
user_details = await get_user_details(user_info["email"])
current_datetime = get_time()
user_details.metadata["last_login"] = current_datetime
# if new user, set the number of tries
if "tokens_left" not in user_details.metadata:
await reset_tokens_for_user(user_details)
if "last_message_time" in user_details.metadata and "admin" not in get_user_role(
user_info["email"]
):
cooldown, _ = check_user_cooldown(user_details, current_datetime)
if cooldown:
return RedirectResponse("/cooldown")
else:
await reset_tokens_for_user(user_details)
if user_info:
username = user_info["email"]
role = get_user_role(username)
jwt_token = request.cookies.get("X-User-Info")
return templates.TemplateResponse(
"dashboard.html",
{
"request": request,
"username": username,
"role": role,
"jwt_token": jwt_token,
"tokens_left": user_details.metadata["tokens_left"],
},
)
return RedirectResponse("/")
@app.get("/start-tutor")
@app.post("/start-tutor")
async def start_tutor(request: Request):
user_info = await get_user_info_from_cookie(request)
if user_info:
user_info_json = json.dumps(user_info)
user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
response = RedirectResponse(CHAINLIT_PATH, status_code=303)
response.set_cookie(key="X-User-Info", value=user_info_encoded, httponly=True)
return response
return RedirectResponse(url="/")
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
if exc.status_code == 404:
return templates.TemplateResponse(
"error_404.html", {"request": request}, status_code=404
)
return templates.TemplateResponse(
"error.html",
{"request": request, "error": str(exc)},
status_code=exc.status_code,
)
@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
return templates.TemplateResponse(
"error.html", {"request": request, "error": str(exc)}, status_code=500
)
@app.get("/logout", response_class=HTMLResponse)
async def logout(request: Request, response: Response):
await del_user_info_from_cookie(request=request, response=response)
response = RedirectResponse(url="/", status_code=302)
# Set cookies to empty values and expire them immediately
response.set_cookie(key="session_token", value="", expires=0)
response.set_cookie(key="X-User-Info", value="", expires=0)
return response
mount_chainlit(app=app, target="main.py", path=CHAINLIT_PATH)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=7860)