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] 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 %}