sdp / main.py
drix60's picture
update code
f5fce04
Raw
History Blame Contribute Delete
7.82 kB
import json
import re
import httpx
from fastapi import Depends, FastAPI, Request
from fastapi.responses import (FileResponse, HTMLResponse, JSONResponse,
RedirectResponse, StreamingResponse)
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from supabase import create_client
from app.auth import Auth0Bearer
from app.config import settings
from app.file_serving import (
HTML_EXTS, HTML_GZ_EXT, IMAGE_EXTS, PDF_EXT,
serve_html_file, serve_html_gz_file, serve_image_file, serve_binary_file
)
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="static")
supabase = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
# --- Load and flatten mapping.json from Supabase bucket ---
def fetch_mapping_json():
resp = supabase.storage.from_(settings.BUCKET_NAME).download("mapping.json")
if hasattr(resp, "read"):
content = resp.read()
else:
content = resp
if isinstance(content, bytes):
content = content.decode("utf-8", errors="replace")
return json.loads(content)
def flatten_mapping(node, parent_path=""):
"""Recursively flattens a nested mapping.json structure into a path:uuid dict."""
mapping = {}
if node["type"] == "file":
full_path = parent_path + node["name"]
mapping[full_path] = node["random_filename"]
elif node["type"] == "folder":
folder_path = parent_path + node["name"] + "/"
for child in node.get("children", []):
mapping.update(flatten_mapping(child, folder_path))
return mapping
mapping_json = fetch_mapping_json()
file_mapping = flatten_mapping(mapping_json)
@app.get("/file/{file_path:path}")
async def serve_file(file_path: str, user=Depends(Auth0Bearer())):
"""Serves HTML, image, or other files from Supabase storage, mapping logical to UUID paths."""
mapped_path = file_mapping.get(f"root/{file_path}", file_path)
file_resp = supabase.storage.from_(settings.BUCKET_NAME).download(mapped_path)
# Serve pre-compressed .html.gz files as gzip
if file_path.endswith(HTML_GZ_EXT):
return serve_html_gz_file(file_resp, file_path)
# Serve .html and .htm files as HTMLResponse (utf-8)
if any(file_path.endswith(ext) for ext in HTML_EXTS):
return serve_html_file(file_resp, file_path)
# Handle image files
if any(file_path.lower().endswith(ext) for ext in IMAGE_EXTS):
return serve_image_file(file_resp, file_path)
# Default: serve as download (binary)
return serve_binary_file(file_resp, file_path)
# Serve raw files for embedding (PDF iframe)
@app.get("/file/raw/{file_path:path}")
async def serve_raw_file(file_path: str, user=Depends(Auth0Bearer())):
mapped_path = file_mapping.get(file_path, file_path)
file_resp = supabase.storage.from_(settings.BUCKET_NAME).download(mapped_path)
# Handle PDF
if file_path.endswith(PDF_EXT):
return serve_binary_file(file_resp, file_path, media_type="application/pdf")
# Default: serve as binary
return serve_binary_file(file_resp, file_path, media_type="application/octet-stream")
@app.get("/login")
async def login():
login_url = (
f"https://{settings.AUTH0_DOMAIN}/authorize?audience={settings.API_AUDIENCE}"
f"&response_type=code&client_id={settings.AUTH0_CLIENT_ID}"
f"&redirect_uri={settings.AUTH0_CALLBACK_URL}"
)
return RedirectResponse(login_url)
@app.get("/logout")
async def logout():
response = RedirectResponse(
f"https://{settings.AUTH0_DOMAIN}/v2/logout?client_id={settings.AUTH0_CLIENT_ID}&returnTo={settings.AUTH0_LOGOUT_REDIRECT_URL}"
)
response.delete_cookie("access_token")
return response
@app.get("/callback", response_class=HTMLResponse)
async def callback(request: Request):
code = request.query_params.get("code")
if not code:
return HTMLResponse("<h2>Login failed: No code found.</h2>", status_code=400)
token_url = f"https://{settings.AUTH0_DOMAIN}/oauth/token"
headers = {"content-type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "authorization_code",
"client_id": settings.AUTH0_CLIENT_ID,
"client_secret": settings.AUTH0_CLIENT_SECRET,
"code": code,
"redirect_uri": settings.AUTH0_CALLBACK_URL,
}
async with httpx.AsyncClient() as client:
token_resp = await client.post(token_url, data=data, headers=headers)
if token_resp.status_code != 200:
return HTMLResponse(
f"<h2>Token exchange failed: {token_resp.text}</h2>", status_code=400
)
tokens = token_resp.json()
access_token = tokens.get("access_token")
if not access_token:
return HTMLResponse(
"<h2>Token exchange failed: No access token.</h2>", status_code=400
)
response = RedirectResponse("/sdp")
response.set_cookie(
key="access_token", value=access_token, httponly=True, secure=False
)
return response
@app.get("/", response_class=HTMLResponse)
async def hello_world(request: Request):
# Render a login button at the top left with dark mode using a template
return templates.TemplateResponse("login.html", {"request": request})
def extract_prefix(name):
match = re.match(r"(\d+)\.", name)
return int(match.group(1)) if match else float('inf')
def render_bootstrap_tree(node, parent_path="", is_root=False):
if node["type"] == "folder":
li_class = "root-folder" if is_root else ""
html = f'<li class="{li_class}">' \
+ f'<span class="folder-toggle">' \
+ f'<i class="bi bi-caret-right-fill caret"></i>' \
+ f'<i class="bi bi-folder-fill"></i>{node["name"]}/' \
+ f'</span>'
if node.get("children"):
children = node["children"]
folders = [c for c in children if c["type"] == "folder"]
files = [c for c in children if c["type"] == "file"]
folders = sorted(folders, key=lambda x: extract_prefix(x["name"]))
files = sorted(files, key=lambda x: extract_prefix(x["name"]))
sorted_children = folders + files
html += '<ul>'
for child in sorted_children:
child_path = parent_path + node["name"] + "/" if node["name"] != "root" else ""
html += render_bootstrap_tree(child, child_path)
html += '</ul>'
html += '</li>'
return html
elif node["type"] == "file":
file_path = parent_path + node["name"]
html = (
f'<li>'
f'<i class="bi bi-file-earmark-text"></i>'
f'<a href="/file/{file_path}">{node["name"]}</a>'
f'</li>'
)
return html
return ""
@app.get("/sdp", response_class=HTMLResponse)
async def show_bucket_tree(request: Request, user=Depends(Auth0Bearer())):
tree_html = "".join(render_bootstrap_tree(child, is_root=True) for child in mapping_json["children"])
return templates.TemplateResponse(
"tree.html", {"request": request, "tree_html": tree_html}
)
@app.api_route("/health", methods=["GET", "HEAD"])
async def health():
return JSONResponse({"status": "ok"})
@app.middleware("http")
async def check_auth(request: Request, call_next):
public_paths = ["/login", "/logout", "/callback", "/health", "/"]
if request.url.path not in public_paths:
auth = request.headers.get("authorization")
cookie_token = request.cookies.get("access_token")
if not auth and not cookie_token:
return RedirectResponse("/login")
response = await call_next(request)
return response