From dd3002d2c85302a8d75bb3f5c253dffc9ce98cad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:17:53 +0000 Subject: [PATCH 1/4] Initial plan From ba387b8e8bdf8f8732ae237397083072d280977e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:21:52 +0000 Subject: [PATCH 2/4] fix(ui): correct OAuth setup page links in integrations dashboard The oauthLink() function in integrations_dashboard.html returned /dropbox, /google-drive, /onedrive which are not valid routes (404). The actual view routes are /dropbox-setup, /google-drive-setup, /onedrive-setup. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- frontend/templates/integrations_dashboard.html | 6 +++--- tests/test_views_integrations.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/templates/integrations_dashboard.html b/frontend/templates/integrations_dashboard.html index 434ba630..48a86a98 100644 --- a/frontend/templates/integrations_dashboard.html +++ b/frontend/templates/integrations_dashboard.html @@ -840,9 +840,9 @@ function integrationsDashboard() { hasFormFields(t) { return TYPES_WITH_FORM_FIELDS.has(t); }, oauthLink(t) { - if (t === 'DROPBOX') return '/dropbox'; - if (t === 'GOOGLE_DRIVE') return '/google-drive'; - if (t === 'ONEDRIVE') return '/onedrive'; + if (t === 'DROPBOX') return '/dropbox-setup'; + if (t === 'GOOGLE_DRIVE') return '/google-drive-setup'; + if (t === 'ONEDRIVE') return '/onedrive-setup'; return '#'; }, diff --git a/tests/test_views_integrations.py b/tests/test_views_integrations.py index 1869ca1d..1bd71f17 100644 --- a/tests/test_views_integrations.py +++ b/tests/test_views_integrations.py @@ -63,6 +63,19 @@ class TestIntegrationsDashboardView: assert response.status_code == 200 assert b"integrationsDashboard" in response.content + def test_integrations_page_oauth_links_point_to_setup_pages(self, client): + """oauthLink() must reference the correct -setup URLs, not bare paths.""" + response = client.get("/integrations") + assert response.status_code == 200 + body = response.text + assert "'/dropbox-setup'" in body or '"/dropbox-setup"' in body + assert "'/google-drive-setup'" in body or '"/google-drive-setup"' in body + assert "'/onedrive-setup'" in body or '"/onedrive-setup"' in body + # Ensure the old broken paths are gone + assert "return '/dropbox'" not in body.replace("/dropbox-setup", "") + assert "return '/google-drive'" not in body.replace("/google-drive-setup", "") + assert "return '/onedrive'" not in body.replace("/onedrive-setup", "") + def test_integrations_page_contains_quota_indicators(self, client): """GET /integrations page includes quota labels.""" response = client.get("/integrations") From 73119e0cef6a50e23784267e8d68ec965a5bc70d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:40:25 +0000 Subject: [PATCH 3/4] feat(auth): save OAuth credentials per-user to UserIntegration records - Add 'Authorize' button to integration cards for OAuth types without credentials - Add isOAuthType() helper and update info box in create/edit modal - Accept integration_id query param in Dropbox, Google Drive, OneDrive setup views - Store integration_id in sessionStorage on setup pages - Add per-user flow in OAuth callbacks: PUT credentials to /api/integrations/{id} - Preserve existing global flow as fallback when no integration_id is present Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/views/dropbox.py | 5 +- app/views/google_drive.py | 5 +- app/views/onedrive.py | 5 +- frontend/templates/dropbox.html | 6 +++ frontend/templates/dropbox_callback.html | 46 +++++++++++++++++- frontend/templates/google_drive.html | 6 +++ frontend/templates/google_drive_callback.html | 47 +++++++++++++++++- .../templates/integrations_dashboard.html | 27 +++++++++-- frontend/templates/onedrive.html | 6 +++ frontend/templates/onedrive_callback.html | 48 ++++++++++++++++++- 10 files changed, 189 insertions(+), 12 deletions(-) diff --git a/app/views/dropbox.py b/app/views/dropbox.py index 62a281ea..e6698f6f 100644 --- a/app/views/dropbox.py +++ b/app/views/dropbox.py @@ -2,7 +2,7 @@ Dropbox integration views for setup and OAuth callback. """ -from fastapi import Request +from fastapi import Query, Request from app.views.base import APIRouter, require_login, settings, templates @@ -11,7 +11,7 @@ router = APIRouter() @router.get("/dropbox-setup") @require_login -async def dropbox_setup_page(request: Request): +async def dropbox_setup_page(request: Request, integration_id: int | None = Query(None)): """ Setup page for the Dropbox integration. Shows configuration status and setup instructions. @@ -28,6 +28,7 @@ async def dropbox_setup_page(request: Request): "app_secret_value": settings.dropbox_app_secret if settings.dropbox_app_secret else "", "refresh_token_value": settings.dropbox_refresh_token if settings.dropbox_refresh_token else "", "folder_path": settings.dropbox_folder or "/Documents/Uploads", # Default folder path + "integration_id": integration_id, }, ) diff --git a/app/views/google_drive.py b/app/views/google_drive.py index 4b67af5f..e6da1f65 100644 --- a/app/views/google_drive.py +++ b/app/views/google_drive.py @@ -4,7 +4,7 @@ Google Drive integration views for setup and OAuth callback. import urllib.parse -from fastapi import Request +from fastapi import Query, Request from fastapi.responses import RedirectResponse from app.views.base import APIRouter, require_login, settings, templates @@ -14,7 +14,7 @@ router = APIRouter() @router.get("/google-drive-setup") @require_login -async def google_drive_setup_page(request: Request): +async def google_drive_setup_page(request: Request, integration_id: int | None = Query(None)): """ Setup page for the Google Drive integration. Shows configuration status and setup instructions. @@ -55,6 +55,7 @@ async def google_drive_setup_page(request: Request): "refresh_token_value": settings.google_drive_refresh_token or "", "folder_id": settings.google_drive_folder_id or "", "has_credentials_json": bool(settings.google_drive_credentials_json), + "integration_id": integration_id, }, ) diff --git a/app/views/onedrive.py b/app/views/onedrive.py index eb20f537..a721c376 100644 --- a/app/views/onedrive.py +++ b/app/views/onedrive.py @@ -2,7 +2,7 @@ OneDrive integration views for setup and OAuth callback. """ -from fastapi import Request +from fastapi import Query, Request from app.views.base import APIRouter, require_login, settings, templates @@ -11,7 +11,7 @@ router = APIRouter() @router.get("/onedrive-setup") @require_login -async def onedrive_setup_page(request: Request): +async def onedrive_setup_page(request: Request, integration_id: int | None = Query(None)): """ Setup page for the OneDrive integration. Shows configuration status and setup instructions. @@ -35,6 +35,7 @@ async def onedrive_setup_page(request: Request): "refresh_token": bool(settings.onedrive_refresh_token), "refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "", "folder_path": settings.onedrive_folder_path or "Documents/Uploads", # Default folder path + "integration_id": integration_id, }, ) diff --git a/frontend/templates/dropbox.html b/frontend/templates/dropbox.html index ae9165b8..6c59b9e9 100644 --- a/frontend/templates/dropbox.html +++ b/frontend/templates/dropbox.html @@ -216,6 +216,12 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }} document.addEventListener('DOMContentLoaded', function() { + // Store integration_id if provided (for per-user OAuth flow) + const integrationId = "{{ integration_id or '' }}"; + if (integrationId) { + sessionStorage.setItem('oauth_integration_id', integrationId); + } + // Elements const startAuthFlowBtn = document.getElementById('start-auth-flow'); const testTokenBtn = document.getElementById('test-token'); diff --git a/frontend/templates/dropbox_callback.html b/frontend/templates/dropbox_callback.html index 623a5d5b..5d253624 100644 --- a/frontend/templates/dropbox_callback.html +++ b/frontend/templates/dropbox_callback.html @@ -97,6 +97,7 @@ document.addEventListener('DOMContentLoaded', function() { const appKey = sessionStorage.getItem('dropbox_app_key') || "{{ app_key_value }}"; const appSecret = sessionStorage.getItem('dropbox_app_secret') || "{{ app_secret_value }}"; const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads'; + const integrationId = sessionStorage.getItem('oauth_integration_id'); const redirectUri = window.location.origin + "/dropbox-callback"; @@ -137,7 +138,50 @@ document.addEventListener('DOMContentLoaded', function() { }) .then(data => { if (data.refresh_token) { - // Update settings in memory + // Per-user flow: save to the user's integration record + if (integrationId) { + const creds = { + refresh_token: data.refresh_token, + app_key: appKey, + app_secret: appSecret, + }; + const cfgUpdate = {}; + if (folderPath) cfgUpdate.folder = folderPath; + + const body = { credentials: creds }; + if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate; + + document.getElementById('processing-message').innerHTML = + '
Saving credentials to your integration...
'; + + return fetch(`/api/integrations/${integrationId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }).then(response => { + if (!response.ok) { + return response.json().then(err => { + throw new Error('Failed to save credentials: ' + (err.detail || 'Unknown error')); + }); + } + return response.json(); + }).then(() => { + // Clean up + sessionStorage.removeItem('dropbox_app_key'); + sessionStorage.removeItem('dropbox_app_secret'); + sessionStorage.removeItem('dropbox_folder_path'); + sessionStorage.removeItem('oauth_integration_id'); + + // Show brief success then redirect to integrations + document.getElementById('processing-message').innerHTML = + '✓ Dropbox authorized successfully!
' + + 'Redirecting to Integrations...
'; + document.querySelector('.animate-spin').parentNode.classList.add('hidden'); + setTimeout(() => { window.location.href = '/integrations'; }, 2000); + }); + } + + // Global flow: update settings in memory const updateFormData = new FormData(); updateFormData.append('refresh_token', data.refresh_token); diff --git a/frontend/templates/google_drive.html b/frontend/templates/google_drive.html index b65c482b..3a964b04 100644 --- a/frontend/templates/google_drive.html +++ b/frontend/templates/google_drive.html @@ -358,6 +358,12 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }} {% block scripts %}