| 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) |
|
|
|
|
| |
| 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) |
| |
| if file_path.endswith(HTML_GZ_EXT): |
| return serve_html_gz_file(file_resp, file_path) |
| |
| if any(file_path.endswith(ext) for ext in HTML_EXTS): |
| return serve_html_file(file_resp, file_path) |
| |
| if any(file_path.lower().endswith(ext) for ext in IMAGE_EXTS): |
| return serve_image_file(file_resp, file_path) |
| |
| return serve_binary_file(file_resp, file_path) |
|
|
| |
| @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) |
| |
| if file_path.endswith(PDF_EXT): |
| return serve_binary_file(file_resp, file_path, media_type="application/pdf") |
| |
| 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): |
| |
| 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 |
|
|