|
| 1 | +"""FastAPI app: themed static UI for Keymaster (mock data until vault exists).""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +from fastapi import FastAPI, Request |
| 8 | +from fastapi.responses import HTMLResponse, RedirectResponse |
| 9 | +from fastapi.staticfiles import StaticFiles |
| 10 | +from fastapi.templating import Jinja2Templates |
| 11 | + |
| 12 | +_PKG_DIR = Path(__file__).resolve().parent |
| 13 | + |
| 14 | +MOCK_PROFILES: list[dict[str, str]] = [ |
| 15 | + { |
| 16 | + "id": "personal", |
| 17 | + "display_name": "Personal", |
| 18 | + "public_key_hex": ( |
| 19 | + "3f8a2c1e9b0d4f7a6e5c8b1d2f0a3948" |
| 20 | + "7e6d5c4b3a291807f6e5d4c3b2a1908f" |
| 21 | + ), |
| 22 | + "created_at": "2026-03-28T14:22:00Z", |
| 23 | + }, |
| 24 | + { |
| 25 | + "id": "organization", |
| 26 | + "display_name": "Organization", |
| 27 | + "public_key_hex": ( |
| 28 | + "a1b2c3d4e5f60718293a4b5c6d7e8f90" |
| 29 | + "0f1e2d3c4b5a69788796a5b4c3d2e1f0" |
| 30 | + ), |
| 31 | + "created_at": "2026-03-29T09:15:00Z", |
| 32 | + }, |
| 33 | +] |
| 34 | + |
| 35 | + |
| 36 | +def _profile_by_id(profile_id: str) -> dict[str, str] | None: |
| 37 | + for p in MOCK_PROFILES: |
| 38 | + if p["id"] == profile_id: |
| 39 | + return p |
| 40 | + return None |
| 41 | + |
| 42 | + |
| 43 | +def create_app() -> FastAPI: |
| 44 | + app = FastAPI(title="Keymaster", version="0.1.0", docs_url=None, redoc_url=None) |
| 45 | + templates = Jinja2Templates(directory=str(_PKG_DIR / "templates")) |
| 46 | + app.mount( |
| 47 | + "/static", |
| 48 | + StaticFiles(directory=str(_PKG_DIR / "static")), |
| 49 | + name="static", |
| 50 | + ) |
| 51 | + |
| 52 | + def ctx(request: Request, **extra: object) -> dict[str, object]: |
| 53 | + out: dict[str, object] = { |
| 54 | + "request": request, |
| 55 | + "ui_preview": True, |
| 56 | + "nav_section": "none", |
| 57 | + } |
| 58 | + out.update(extra) |
| 59 | + return out |
| 60 | + |
| 61 | + @app.get("/", response_class=RedirectResponse) |
| 62 | + async def root() -> RedirectResponse: |
| 63 | + return RedirectResponse(url="/unlock", status_code=302) |
| 64 | + |
| 65 | + @app.get("/unlock", response_class=HTMLResponse) |
| 66 | + async def unlock(request: Request) -> HTMLResponse: |
| 67 | + return templates.TemplateResponse( |
| 68 | + request, |
| 69 | + "unlock.html", |
| 70 | + ctx(request, page_title="Unlock vault", nav_section="unlock"), |
| 71 | + ) |
| 72 | + |
| 73 | + @app.get("/setup", response_class=HTMLResponse) |
| 74 | + async def setup(request: Request) -> HTMLResponse: |
| 75 | + return templates.TemplateResponse( |
| 76 | + request, |
| 77 | + "setup.html", |
| 78 | + ctx(request, page_title="Create vault", nav_section="setup"), |
| 79 | + ) |
| 80 | + |
| 81 | + @app.get("/identities", response_class=HTMLResponse) |
| 82 | + async def identities(request: Request) -> HTMLResponse: |
| 83 | + return templates.TemplateResponse( |
| 84 | + request, |
| 85 | + "dashboard.html", |
| 86 | + ctx( |
| 87 | + request, |
| 88 | + page_title="Identities", |
| 89 | + profiles=MOCK_PROFILES, |
| 90 | + nav_section="identities", |
| 91 | + ), |
| 92 | + ) |
| 93 | + |
| 94 | + @app.get("/identities/{profile_id}", response_class=HTMLResponse) |
| 95 | + async def identity_detail(request: Request, profile_id: str) -> HTMLResponse: |
| 96 | + profile = _profile_by_id(profile_id) |
| 97 | + if profile is None: |
| 98 | + return templates.TemplateResponse( |
| 99 | + request, |
| 100 | + "not_found.html", |
| 101 | + ctx(request, page_title="Not found", nav_section="none"), |
| 102 | + status_code=404, |
| 103 | + ) |
| 104 | + return templates.TemplateResponse( |
| 105 | + request, |
| 106 | + "profile_detail.html", |
| 107 | + ctx( |
| 108 | + request, |
| 109 | + page_title=profile["display_name"], |
| 110 | + profile=profile, |
| 111 | + nav_section="detail", |
| 112 | + ), |
| 113 | + ) |
| 114 | + |
| 115 | + return app |
0 commit comments