From d86a185e04d8d44408c0c1ba4d64f6c4bcc9e69c Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Tue, 11 Nov 2025 20:53:43 +0100 Subject: [PATCH] refactor: move webapp from /user/page to /app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified the webapp routing structure by consolidating the admin UI to a single clean endpoint. Changes: - Moved webapp from /user/page to /app (root of mount) - Removed /user JSON endpoint (no longer needed) - Updated mount point from /user to /app in app.py - Updated all route path checks (3 locations) - Updated OAuth redirects to point to /app - Updated all HTMX endpoint references - Updated documentation (ADR-007, CHANGELOG) - Added redirect from /app to /app/ for trailing slash handling New Route Structure: - /app - Main webapp (HTML UI with tabs) - /app/revoke - Revoke background access - /app/webhooks - Webhook management UI - /app/webhooks/enable/{preset_id} - Enable webhook preset - /app/webhooks/disable/{preset_id} - Disable webhook preset Breaking Change: Existing bookmarks to /user or /user/page will no longer work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- ...7-background-vector-sync-job-management.md | 2 +- nextcloud_mcp_server/app.py | 31 ++++++++++--------- .../auth/browser_oauth_routes.py | 10 +++--- nextcloud_mcp_server/auth/userinfo_routes.py | 2 +- nextcloud_mcp_server/auth/webhook_routes.py | 10 +++--- 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abacc8a..0051506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,7 +86,7 @@ - implement ADR-009 - refactor semantic search to use generic semantic:read scope - implement MCP sampling for semantic search RAG (ADR-008) - add optional vector database and semantic search to helm chart -- add vector sync processing status to /user/page endpoint +- add vector sync processing status to /app endpoint - implement semantic search tool and fix vector sync issues (ADR-007 Phase 3) - implement vector sync scanner and processor (ADR-007 Phase 2) diff --git a/docs/ADR-007-background-vector-sync-job-management.md b/docs/ADR-007-background-vector-sync-job-management.md index b1fe052..0e7f817 100644 --- a/docs/ADR-007-background-vector-sync-job-management.md +++ b/docs/ADR-007-background-vector-sync-job-management.md @@ -377,7 +377,7 @@ async def get_vector_sync_status(ctx: Context) -> dict: } ``` -The web UI (`/user/page` route) mirrors these controls with a simple toggle switch for enabling/disabling sync and a status display showing indexed counts and sync state. There is no job history, no detailed progress bars, no per-document status—just the essential information users need. +The web UI (`/app` route) mirrors these controls with a simple toggle switch for enabling/disabling sync and a status display showing indexed counts and sync state. There is no job history, no detailed progress bars, no per-document status—just the essential information users need. ### Authentication and Offline Access diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index f25b107..c8909f7 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -21,7 +21,7 @@ from pydantic import AnyHttpUrl from starlette.applications import Starlette from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.cors import CORSMiddleware -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, RedirectResponse from starlette.routing import Mount, Route from nextcloud_mcp_server.auth import ( @@ -1038,7 +1038,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): # browser_app is in the same function scope (defined later in create_app) # We need to find it in the mounted routes for route in app.routes: - if isinstance(route, Mount) and route.path == "/user": + if isinstance(route, Mount) and route.path == "/app": route.app.state.oauth_context = oauth_context_dict logger.info( "OAuth context shared with browser_app for session auth" @@ -1059,7 +1059,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): # Also share with browser_app for webhook routes for route in app.routes: - if isinstance(route, Mount) and route.path == "/user": + if isinstance(route, Mount) and route.path == "/app": route.app.state.storage = storage logger.info( "Storage shared with browser_app for webhook management" @@ -1099,15 +1099,15 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): app.state.shutdown_event = shutdown_event app.state.scanner_wake_event = scanner_wake_event - # Also share with browser_app for /user/page route + # Also share with browser_app for /app route for route in app.routes: - if isinstance(route, Mount) and route.path == "/user": + if isinstance(route, Mount) and route.path == "/app": route.app.state.document_send_stream = send_stream route.app.state.document_receive_stream = receive_stream route.app.state.shutdown_event = shutdown_event route.app.state.scanner_wake_event = scanner_wake_event logger.info( - "Vector sync state shared with browser_app for /user/page" + "Vector sync state shared with browser_app for /app" ) break @@ -1410,7 +1410,6 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): from nextcloud_mcp_server.auth.userinfo_routes import ( revoke_session, user_info_html, - user_info_json, ) from nextcloud_mcp_server.auth.webhook_routes import ( disable_webhook_preset, @@ -1421,13 +1420,12 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): # Create a separate Starlette app for browser routes that need session auth # This prevents SessionAuthBackend from interfering with FastMCP's OAuth browser_routes = [ - Route("/", user_info_json, methods=["GET"]), # /user/ → user_info_json - Route("/page", user_info_html, methods=["GET"]), # /user/page → user_info_html + Route("/", user_info_html, methods=["GET"]), # /app → webapp (HTML UI) Route( "/revoke", revoke_session, methods=["POST"], name="revoke_session_endpoint" - ), # /user/revoke → revoke_session + ), # /app/revoke → revoke_session # Webhook management routes (admin-only) - Route("/webhooks", webhook_management_pane, methods=["GET"]), # /user/webhooks + Route("/webhooks", webhook_management_pane, methods=["GET"]), # /app/webhooks Route( "/webhooks/enable/{preset_id:str}", enable_webhook_preset, methods=["POST"] ), @@ -1444,9 +1442,14 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): backend=SessionAuthBackend(oauth_enabled=oauth_enabled), ) - # Mount browser app at /user (so /user and /user/page work) - routes.append(Mount("/user", app=browser_app)) - logger.info("User info routes with session auth: /user, /user/page") + # Add redirect from /app to /app/ (Starlette requires trailing slash for mounted apps) + routes.append( + Route("/app", lambda request: RedirectResponse("/app/", status_code=307)) + ) + + # Mount browser app at /app (webapp and admin routes) + routes.append(Mount("/app", app=browser_app)) + logger.info("App routes with session auth: /app, /app/webhooks, /app/revoke") # Mount FastMCP at root last (catch-all, handles OAuth via token_verifier) routes.append(Mount("/", app=mcp_app)) diff --git a/nextcloud_mcp_server/auth/browser_oauth_routes.py b/nextcloud_mcp_server/auth/browser_oauth_routes.py index 34fb620..7bc2d00 100644 --- a/nextcloud_mcp_server/auth/browser_oauth_routes.py +++ b/nextcloud_mcp_server/auth/browser_oauth_routes.py @@ -1,7 +1,7 @@ """Browser-based OAuth login routes for admin UI. Separate from MCP OAuth flow - these routes establish browser sessions -for accessing admin UI endpoints like /user/page. +for accessing admin UI endpoints like /app. """ import hashlib @@ -38,8 +38,8 @@ async def oauth_login(request: Request) -> RedirectResponse | JSONResponse: """ oauth_ctx = request.app.state.oauth_context if not oauth_ctx: - # BasicAuth mode - no login needed, redirect to user page - return RedirectResponse("/user/page", status_code=302) + # BasicAuth mode - no login needed, redirect to app + return RedirectResponse("/app", status_code=302) storage = oauth_ctx["storage"] oauth_client = oauth_ctx["oauth_client"] @@ -71,7 +71,7 @@ async def oauth_login(request: Request) -> RedirectResponse | JSONResponse: await storage.store_oauth_session( session_id=state, # Use state as session ID client_id="browser-ui", - client_redirect_uri="/user/page", + client_redirect_uri="/app", state=state, code_challenge=code_challenge, code_challenge_method="S256", @@ -383,7 +383,7 @@ async def oauth_login_callback(request: Request) -> RedirectResponse | HTMLRespo # Continue anyway - profile cache is optional for browser UI # Create response and set session cookie - response = RedirectResponse("/user/page", status_code=302) + response = RedirectResponse("/app", status_code=302) response.set_cookie( key="mcp_session", value=user_id, diff --git a/nextcloud_mcp_server/auth/userinfo_routes.py b/nextcloud_mcp_server/auth/userinfo_routes.py index 7f1f5b2..0aff68e 100644 --- a/nextcloud_mcp_server/auth/userinfo_routes.py +++ b/nextcloud_mcp_server/auth/userinfo_routes.py @@ -602,7 +602,7 @@ async def user_info_html(request: Request) -> HTMLResponse: webhooks_tab_html = "" if show_webhooks_tab: webhooks_tab_html = """ -
+

Loading webhook management...

""" diff --git a/nextcloud_mcp_server/auth/webhook_routes.py b/nextcloud_mcp_server/auth/webhook_routes.py index bec9ca1..a693930 100644 --- a/nextcloud_mcp_server/auth/webhook_routes.py +++ b/nextcloud_mcp_server/auth/webhook_routes.py @@ -32,7 +32,7 @@ def _get_storage(request: Request): Returns: RefreshTokenStorage instance or None """ - # Try browser_app state first (for /user routes) + # Try browser_app state first (for /app routes) storage = getattr(request.app.state, "storage", None) # Try oauth_context if in OAuth mode @@ -289,7 +289,7 @@ async def webhook_management_pane(request: Request) -> HTMLResponse: status_badge = f'✓ Enabled ({num_webhooks} webhooks)' action_button = f"""