diff --git a/.coverage b/.coverage index c279b8d..d27ba23 100644 Binary files a/.coverage and b/.coverage differ diff --git a/src/intervalsicu_mcp_ui/app.py b/src/intervalsicu_mcp_ui/app.py index 384870b..2302cbd 100644 --- a/src/intervalsicu_mcp_ui/app.py +++ b/src/intervalsicu_mcp_ui/app.py @@ -43,18 +43,25 @@ def create_app(config: Config | None = None) -> FastAPI: client_kwargs={"scope": cfg.oidc_scopes}, ) - app = FastAPI(title="Intervals.icu MCP portal") + app = FastAPI(title="Intervals.icu MCP portal", root_path=cfg.root_path) app.add_middleware(SessionMiddleware, secret_key=cfg.session_secret, https_only=True, same_site="lax") app.state.cfg = cfg app.state.oauth = oauth def render(request, name, **ctx): - return _TEMPLATES.TemplateResponse(request, name, {"user": current_user(request), **ctx}) + return _TEMPLATES.TemplateResponse( + request, + name, + {"user": current_user(request), "base": cfg.root_path, "mcp_url": cfg.mcp_url, **ctx}, + ) + + def redirect(path: str, status_code: int = 307): + return RedirectResponse(f"{cfg.root_path}{path}", status_code=status_code) # ----- auth ----------------------------------------------------------- # @app.get("/", response_class=HTMLResponse) async def index(request: Request, user: dict | None = Depends(current_user)): - return RedirectResponse("/account" if user else "/login") + return redirect("/account" if user else "/login") @app.get("/login", response_class=HTMLResponse) async def login(request: Request): @@ -70,7 +77,7 @@ def create_app(config: Config | None = None) -> FastAPI: claims = dict(token.get("userinfo") or {}) sub = claims.get("sub") if not sub: - return RedirectResponse("/login") + return redirect("/login") email = claims.get("email", "") name = claims.get("name") groups = claims.get("groups") or [] @@ -82,18 +89,18 @@ def create_app(config: Config | None = None) -> FastAPI: "name": name, "is_admin": cfg.admin_group in groups, } - return RedirectResponse("/account") + return redirect("/account") @app.get("/logout") async def logout(request: Request): request.session.clear() - return RedirectResponse("/login") + return redirect("/login") # ----- account -------------------------------------------------------- # @app.get("/account", response_class=HTMLResponse) async def account(request: Request, user: dict | None = Depends(current_user)): if not user: - return RedirectResponse("/login") + return redirect("/login") async with db.sessionmaker()() as session: record = await db.get_user(session, user["sub"]) return render(request, "account.html", record=record) @@ -106,7 +113,7 @@ def create_app(config: Config | None = None) -> FastAPI: user: dict | None = Depends(current_user), ): if not user: - return RedirectResponse("/login") + return redirect("/login") ok, message = await validate_credentials(cfg.intervals_api_base, athlete_id, api_key) async with db.sessionmaker()() as session: if ok: @@ -124,7 +131,7 @@ def create_app(config: Config | None = None) -> FastAPI: @app.get("/admin", response_class=HTMLResponse) async def admin(request: Request, admin_user: dict | None = Depends(require_admin)): if not admin_user: - return RedirectResponse("/account") + return redirect("/account") async with db.sessionmaker()() as session: users = await db.list_users(session) return render(request, "admin.html", users=users) @@ -135,7 +142,7 @@ def create_app(config: Config | None = None) -> FastAPI: admin_user: dict | None = Depends(require_admin), ): if not admin_user: - return RedirectResponse("/account") + return redirect("/account") async with db.sessionmaker()() as session: if action == "enable": await db.set_enabled(session, sub, True) @@ -143,7 +150,7 @@ def create_app(config: Config | None = None) -> FastAPI: await db.set_enabled(session, sub, False) elif action == "delete": await db.delete_user(session, sub) - return RedirectResponse("/admin", status_code=303) + return redirect("/admin", status_code=303) @app.get("/healthz") async def healthz(): diff --git a/src/intervalsicu_mcp_ui/config.py b/src/intervalsicu_mcp_ui/config.py index 1d1d143..dd699c8 100644 --- a/src/intervalsicu_mcp_ui/config.py +++ b/src/intervalsicu_mcp_ui/config.py @@ -17,6 +17,8 @@ class Config: admin_group: str intervals_api_base: str oidc_scopes: str + root_path: str # public path prefix when served behind a gateway (e.g. "/portal") + mcp_url: str # the MCP connector URL to show users def load_config() -> Config: @@ -30,4 +32,6 @@ def load_config() -> Config: admin_group=os.environ.get("ADMIN_GROUP", "intervalsicu-mcp-admins"), intervals_api_base=os.environ.get("INTERVALS_API_BASE_URL", "https://intervals.icu/api/v1"), oidc_scopes=os.environ.get("OIDC_SCOPES", "openid email profile groups"), + root_path=os.environ.get("ROOT_PATH", "").rstrip("/"), + mcp_url=os.environ.get("MCP_URL", "https://intervalsicu.farhoodlabs.com/mcp"), ) diff --git a/src/intervalsicu_mcp_ui/templates/account.html b/src/intervalsicu_mcp_ui/templates/account.html index f163c54..109e788 100644 --- a/src/intervalsicu_mcp_ui/templates/account.html +++ b/src/intervalsicu_mcp_ui/templates/account.html @@ -20,7 +20,7 @@

Find these in Intervals.icu → Settings → Developer. Your athlete ID looks like i123456.

-
+ @@ -30,5 +30,5 @@

Connect Claude

Add a custom connector in Claude pointing at:

-

https://intervalsicu-mcp.farhoodlabs.com/mcp

+

{{ mcp_url }}

{% endblock %} diff --git a/src/intervalsicu_mcp_ui/templates/admin.html b/src/intervalsicu_mcp_ui/templates/admin.html index aafc219..0968feb 100644 --- a/src/intervalsicu_mcp_ui/templates/admin.html +++ b/src/intervalsicu_mcp_ui/templates/admin.html @@ -15,11 +15,11 @@ {{ u.last_login_at.strftime('%Y-%m-%d') if u.last_login_at else '—' }} {% if u.enabled %} -
+
{% else %} -
+
{% endif %} -
+
{% endfor %} diff --git a/src/intervalsicu_mcp_ui/templates/base.html b/src/intervalsicu_mcp_ui/templates/base.html index c0a7553..c1722d6 100644 --- a/src/intervalsicu_mcp_ui/templates/base.html +++ b/src/intervalsicu_mcp_ui/templates/base.html @@ -34,9 +34,9 @@ Intervals.icu MCP {% if user %} {% endif %} diff --git a/src/intervalsicu_mcp_ui/templates/login.html b/src/intervalsicu_mcp_ui/templates/login.html index c38c1dd..0d45def 100644 --- a/src/intervalsicu_mcp_ui/templates/login.html +++ b/src/intervalsicu_mcp_ui/templates/login.html @@ -2,5 +2,5 @@ {% block content %}

Intervals.icu MCP portal

Sign in to connect your Intervals.icu account to the MCP server and manage your API credentials.

-

+

{% endblock %} diff --git a/tests/test_app.py b/tests/test_app.py index e2d366f..776e76e 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -41,6 +41,8 @@ def app_url(tmp_path, monkeypatch): admin_group="intervalsicu-mcp-admins", intervals_api_base="https://intervals.icu/api/v1", oidc_scopes="openid email profile groups", + root_path="", + mcp_url="https://intervalsicu.farhoodlabs.com/mcp", ) return appmod.create_app(cfg), url