Better static files handling on backend, when in dev mode: fetch from vite.

This commit is contained in:
2025-12-04 02:30:02 +00:00
parent b9b1c995f9
commit a6591a1fbb
8 changed files with 65 additions and 20 deletions
+2 -3
View File
@@ -244,9 +244,8 @@ def main():
run_kwargs["port"] = port
if devmode:
if os.environ.get("PASSKEY_BUN_PARENT") != "1":
os.environ["PASSKEY_BUN_PARENT"] = "1"
frontend.run_dev()
os.environ["PASSKEY_DEVMODE"] = "1"
frontend.run_dev()
if all_ifaces and not uds:
if devmode:
+4 -4
View File
@@ -2,8 +2,8 @@ import logging
from datetime import timezone
from uuid import UUID, uuid4
from fastapi import Body, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi import Body, FastAPI, HTTPException, Request, Response
from fastapi.responses import JSONResponse
from ..authsession import reset_expires
from ..globals import db
@@ -33,7 +33,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
"""Handle AuthException with auth info for UI."""
return JSONResponse(
status_code=exc.status_code,
content=authz.auth_error_content(exc),
content=await authz.auth_error_content(exc),
)
@@ -45,7 +45,7 @@ async def general_exception_handler(_request, exc: Exception):
@app.get("/")
async def adminapp(request: Request, auth=AUTH_COOKIE):
return FileResponse(frontend.file("auth", "admin", "index.html"))
return Response(*await frontend.read("/auth/admin/index.html"))
# -------------------- Organizations --------------------
+3 -3
View File
@@ -62,7 +62,7 @@ async def auth_exception_handler(_request: Request, exc: authz.AuthException):
"""Handle AuthException with auth info for UI."""
return JSONResponse(
status_code=exc.status_code,
content=authz.auth_error_content(exc),
content=await authz.auth_error_content(exc),
)
@@ -179,7 +179,7 @@ async def forward_authentication(
if wants_html:
# Browser request - return full-page HTML with metadata
data_attrs = {"mode": e.mode, **e.metadata}
html = frontend.file("int", "forward", "index.html").read_bytes()
html = (await frontend.read("/int/forward/index.html"))[0]
html = htmlutil.patch_html_data_attrs(html, **data_attrs)
return Response(
html, status_code=e.status_code, media_type="text/html; charset=UTF-8"
@@ -188,7 +188,7 @@ async def forward_authentication(
# API request - return JSON with iframe srcdoc HTML
return JSONResponse(
status_code=e.status_code,
content=authz.auth_error_content(e),
content=await authz.auth_error_content(e),
)
+2 -2
View File
@@ -32,13 +32,13 @@ class AuthException(HTTPException):
self.metadata = metadata
def auth_error_content(exc: AuthException) -> dict:
async def auth_error_content(exc: AuthException) -> dict:
"""Generate JSON response content for an AuthException.
Returns a dict with detail, mode, and iframe HTML for srcdoc embedding.
"""
data_attrs = {"mode": exc.mode, **exc.metadata}
iframe_html = frontend.file("auth", "restricted", "index.html").read_bytes()
iframe_html = (await frontend.read("/auth/restricted/index.html"))[0]
iframe_html = htmlutil.patch_html_data_attrs(iframe_html, **data_attrs)
return {
"detail": exc.detail,
+5 -5
View File
@@ -3,7 +3,7 @@ import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from passkey.util import frontend, hostutil, passphrase
@@ -64,7 +64,7 @@ app.mount(
@app.get("/auth/restricted/")
async def restricted_view():
"""Serve the restricted/authentication UI for iframe embedding."""
return FileResponse(frontend.file("auth", "restricted", "index.html"))
return Response(*await frontend.read("/auth/restricted/index.html"))
# Navigable URLs are defined here. We support both / and /auth/ as the base path
@@ -83,8 +83,8 @@ async def frontapp(request: Request, response: Response, auth=AUTH_COOKIE):
cur_host = hostutil.normalize_host(request.headers.get("host"))
cfg_normalized = hostutil.normalize_host(cfg_host)
if cur_host and cfg_normalized and cur_host != cfg_normalized:
return FileResponse(frontend.file("int", "host", "index.html"))
return FileResponse(frontend.file("auth", "index.html"))
return Response(*await frontend.read("/int/host/index.html"))
return Response(*await frontend.read("/auth/index.html"))
@app.get("/admin", include_in_schema=False)
@@ -105,4 +105,4 @@ async def reset_link(reset: str):
"""Serve the reset app directly with an injected reset token."""
if not passphrase.is_well_formed(reset):
raise HTTPException(status_code=404)
return FileResponse(frontend.file("int", "reset", "index.html"))
return Response(*await frontend.read("/int/reset/index.html"))
+1 -1
View File
@@ -29,7 +29,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
"""Handle AuthException with auth info for UI."""
return JSONResponse(
status_code=exc.status_code,
content=authz.auth_error_content(exc),
content=await authz.auth_error_content(exc),
)
+1 -1
View File
@@ -26,7 +26,7 @@ def websocket_error_handler(func):
await ws.send_json(
{
"status": e.status_code,
**authz.auth_error_content(e),
**(await authz.auth_error_content(e)),
}
)
except (ValueError, InvalidAuthenticationResponse) as e:
+47 -1
View File
@@ -1,4 +1,7 @@
import asyncio
import atexit
import mimetypes
import os
import shutil
import signal
import subprocess
@@ -7,7 +10,11 @@ from pathlib import Path
from sys import stderr
from threading import Thread
__all__ = ["path", "file", "run_dev"]
import httpx
__all__ = ["path", "file", "read", "run_dev"]
DEV_SERVER = "http://localhost:4403"
NO_FRONTEND_TOOL = """\
┃ ⚠️ deno, npm or bunx needed to run the frontend server.
@@ -53,6 +60,45 @@ def file(*parts: str) -> Path:
return path.joinpath(*parts)
def _is_dev_mode() -> bool:
"""Check if we're running in dev mode (Vite frontend server)."""
return os.environ.get("PASSKEY_DEVMODE") == "1"
async def read(filepath: str) -> tuple[bytes, int, dict[str, str]]:
"""Read file content and return response tuple.
In dev mode, fetches from the Vite dev server.
In production, reads from the static build directory.
Args:
filepath: Path relative to frontend root, e.g. "/auth/index.html"
Returns:
Tuple of (content, status_code, headers) suitable for
FastAPI Response(*args) or Sanic raw response.
"""
if _is_dev_mode():
async with httpx.AsyncClient() as client:
resp = await client.get(f"{DEV_SERVER}{filepath}")
resp.raise_for_status()
mime = resp.headers.get("content-type", "application/octet-stream")
# Strip charset suffix if present
mime = mime.split(";")[0].strip()
return resp.content, resp.status_code, {"content-type": mime}
else:
# Production: read from static build
file_path = path / filepath.lstrip("/")
content = await _read_file_async(file_path)
mime, _ = mimetypes.guess_type(str(file_path))
return content, 200, {"content-type": mime or "application/octet-stream"}
async def _read_file_async(file_path: Path) -> bytes:
"""Read file asynchronously using asyncio.to_thread."""
return await asyncio.to_thread(file_path.read_bytes)
def run_dev():
"""Spawn the frontend dev server (deno, npm, or bunx) as a background process."""
devpath = Path(__file__).parent.parent.parent / "frontend"