From 80b1b01beb1d5c161a6cfc0454800bb3715fa627 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:58:17 +0000 Subject: [PATCH 1/5] Initial plan From c71f33a214272badb5c719f7a91d46a352caa2d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:24:07 +0000 Subject: [PATCH 2/5] feat(integrations): add per-user OAuth wizard with user-mode for Dropbox, OneDrive, Google Drive - Add user_mode to dropbox/onedrive/google_drive setup views that loads integration config - Show user-friendly auth wizard when integration_id is provided (user mode) - In user mode: show integration name, current folder, back-to-integrations link - In callback templates: only save credentials (not config) for user integrations - In integrations dashboard: show Authorize/Re-Authorize button for all OAuth types - Add WATCH_FOLDER OAuth support: detect source_type in config for auth button - isOAuthType() and oauthLink() now accept full integration object Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/views/dropbox.py | 61 +++++- app/views/google_drive.py | 67 +++++-- app/views/onedrive.py | 66 ++++++- frontend/templates/dropbox.html | 85 +++++++-- frontend/templates/dropbox_callback.html | 5 +- frontend/templates/google_drive.html | 174 ++++++++++++------ frontend/templates/google_drive_callback.html | 5 +- .../templates/integrations_dashboard.html | 48 +++-- frontend/templates/onedrive.html | 88 +++++++-- frontend/templates/onedrive_callback.html | 5 +- 10 files changed, 473 insertions(+), 131 deletions(-) diff --git a/app/views/dropbox.py b/app/views/dropbox.py index e6698f6f..f623a7c6 100644 --- a/app/views/dropbox.py +++ b/app/views/dropbox.py @@ -2,33 +2,82 @@ Dropbox integration views for setup and OAuth callback. """ -from fastapi import Query, Request +import json -from app.views.base import APIRouter, require_login, settings, templates +from fastapi import Query, Request +from sqlalchemy.orm import Session + +from app.models import UserIntegration +from app.utils.user_scope import get_current_owner_id +from app.views.base import APIRouter, Depends, get_db, require_login, settings, templates router = APIRouter() @router.get("/dropbox-setup") @require_login -async def dropbox_setup_page(request: Request, integration_id: int | None = Query(None)): +async def dropbox_setup_page( + request: Request, + integration_id: int | None = Query(None), + db: Session = Depends(get_db), +): """ Setup page for the Dropbox integration. - Shows configuration status and setup instructions. + + When ``integration_id`` is provided the page operates in **user mode**: + the OAuth wizard saves credentials to the named per-user integration + record rather than to the global application settings. Only the folder + path from the integration's existing config is pre-populated; global + admin credentials are never exposed in this mode. """ - # Check Dropbox configuration + if integration_id is not None: + owner_id = get_current_owner_id(request) + integration = ( + db.query(UserIntegration) + .filter(UserIntegration.id == integration_id, UserIntegration.owner_id == owner_id) + .first() + ) + if integration: + cfg: dict = {} + if integration.config: + try: + cfg = json.loads(integration.config) + except (json.JSONDecodeError, TypeError): + cfg = {} + # Support both "folder" (DROPBOX destination) and "folder_path" (WATCH_FOLDER source) + folder_path = cfg.get("folder", cfg.get("folder_path", "")) + return templates.TemplateResponse( + "dropbox.html", + { + "request": request, + "user_mode": True, + "is_configured": bool(integration.credentials), + "integration_id": integration_id, + "integration_name": integration.name, + "integration_type": integration.integration_type, + "folder_path": folder_path, + "app_key_value": "", + "app_secret_value": "", + "refresh_token_value": "", + }, + ) + + # ── Admin / global mode ────────────────────────────────────────────────── is_configured = bool(settings.dropbox_app_key and settings.dropbox_app_secret and settings.dropbox_refresh_token) return templates.TemplateResponse( "dropbox.html", { "request": request, + "user_mode": False, "is_configured": is_configured, "app_key_value": settings.dropbox_app_key or "", "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 + "folder_path": settings.dropbox_folder or "/Documents/Uploads", "integration_id": integration_id, + "integration_name": None, + "integration_type": None, }, ) diff --git a/app/views/google_drive.py b/app/views/google_drive.py index e6da1f65..f6bde9c0 100644 --- a/app/views/google_drive.py +++ b/app/views/google_drive.py @@ -2,47 +2,90 @@ Google Drive integration views for setup and OAuth callback. """ +import json import urllib.parse from fastapi import Query, Request from fastapi.responses import RedirectResponse +from sqlalchemy.orm import Session -from app.views.base import APIRouter, require_login, settings, templates +from app.models import UserIntegration +from app.utils.user_scope import get_current_owner_id +from app.views.base import APIRouter, Depends, get_db, require_login, settings, templates router = APIRouter() @router.get("/google-drive-setup") @require_login -async def google_drive_setup_page(request: Request, integration_id: int | None = Query(None)): +async def google_drive_setup_page( + request: Request, + integration_id: int | None = Query(None), + db: Session = Depends(get_db), +): """ Setup page for the Google Drive integration. - Shows configuration status and setup instructions. + + When ``integration_id`` is provided the page operates in **user mode**: + the OAuth wizard saves credentials to the named per-user integration + record rather than to the global application settings. """ - # Check if using OAuth + if integration_id is not None: + owner_id = get_current_owner_id(request) + integration = ( + db.query(UserIntegration) + .filter(UserIntegration.id == integration_id, UserIntegration.owner_id == owner_id) + .first() + ) + if integration: + cfg: dict = {} + if integration.config: + try: + cfg = json.loads(integration.config) + except (json.JSONDecodeError, TypeError): + cfg = {} + folder_id = cfg.get("folder_id", "") + return templates.TemplateResponse( + "google_drive.html", + { + "request": request, + "user_mode": True, + "is_configured": bool(integration.credentials), + "integration_id": integration_id, + "integration_name": integration.name, + "integration_type": integration.integration_type, + "folder_id": folder_id, + "use_oauth": True, + "oauth_configured": bool(integration.credentials), + "sa_configured": False, + "client_id": False, + "client_id_value": "", + "client_secret": False, + "client_secret_value": "", + "refresh_token": False, + "refresh_token_value": "", + "has_credentials_json": False, + }, + ) + + # ── Admin / global mode ────────────────────────────────────────────────── use_oauth = getattr(settings, "google_drive_use_oauth", False) - # Check Google Drive OAuth configuration oauth_configured = bool( settings.google_drive_client_id and settings.google_drive_client_secret and settings.google_drive_refresh_token ) - - # Check Google Drive service account configuration sa_configured = bool(settings.google_drive_credentials_json) - - # Overall configuration status is_configured = (use_oauth and oauth_configured) or (not use_oauth and sa_configured) - if settings.google_drive_folder_id: is_configured = is_configured and True else: is_configured = False - # Get configuration values to display status (hide sensitive values) return templates.TemplateResponse( "google_drive.html", { "request": request, + "user_mode": False, "is_configured": is_configured, "use_oauth": use_oauth, "oauth_configured": oauth_configured, @@ -56,6 +99,8 @@ async def google_drive_setup_page(request: Request, integration_id: int | None = "folder_id": settings.google_drive_folder_id or "", "has_credentials_json": bool(settings.google_drive_credentials_json), "integration_id": integration_id, + "integration_name": None, + "integration_type": None, }, ) diff --git a/app/views/onedrive.py b/app/views/onedrive.py index a721c376..4b8b3763 100644 --- a/app/views/onedrive.py +++ b/app/views/onedrive.py @@ -2,40 +2,90 @@ OneDrive integration views for setup and OAuth callback. """ -from fastapi import Query, Request +import json -from app.views.base import APIRouter, require_login, settings, templates +from fastapi import Query, Request +from sqlalchemy.orm import Session + +from app.models import UserIntegration +from app.utils.user_scope import get_current_owner_id +from app.views.base import APIRouter, Depends, get_db, require_login, settings, templates router = APIRouter() @router.get("/onedrive-setup") @require_login -async def onedrive_setup_page(request: Request, integration_id: int | None = Query(None)): +async def onedrive_setup_page( + request: Request, + integration_id: int | None = Query(None), + db: Session = Depends(get_db), +): """ Setup page for the OneDrive integration. - Shows configuration status and setup instructions. + + When ``integration_id`` is provided the page operates in **user mode**: + the OAuth wizard saves credentials to the named per-user integration + record rather than to the global application settings. """ - # Check OneDrive configuration + if integration_id is not None: + owner_id = get_current_owner_id(request) + integration = ( + db.query(UserIntegration) + .filter(UserIntegration.id == integration_id, UserIntegration.owner_id == owner_id) + .first() + ) + if integration: + cfg: dict = {} + if integration.config: + try: + cfg = json.loads(integration.config) + except (json.JSONDecodeError, TypeError): + cfg = {} + # Support both "folder_path" (WATCH_FOLDER / ONEDRIVE destination) + folder_path = cfg.get("folder_path", cfg.get("folder", "")) + return templates.TemplateResponse( + "onedrive.html", + { + "request": request, + "user_mode": True, + "is_configured": bool(integration.credentials), + "integration_id": integration_id, + "integration_name": integration.name, + "integration_type": integration.integration_type, + "folder_path": folder_path, + "client_id": False, + "client_id_value": "", + "client_secret": False, + "client_secret_value": "", + "tenant_id": "common", + "refresh_token": False, + "refresh_token_value": "", + }, + ) + + # ── Admin / global mode ────────────────────────────────────────────────── is_configured = bool( settings.onedrive_client_id and settings.onedrive_client_secret and settings.onedrive_refresh_token ) - # Get configuration values to display status (hide sensitive values) return templates.TemplateResponse( "onedrive.html", { "request": request, + "user_mode": False, "is_configured": is_configured, "client_id": bool(settings.onedrive_client_id), - "client_id_value": settings.onedrive_client_id or "", # Pass the actual value for the form + "client_id_value": settings.onedrive_client_id or "", "client_secret": bool(settings.onedrive_client_secret), "client_secret_value": settings.onedrive_client_secret if settings.onedrive_client_secret else "", "tenant_id": settings.onedrive_tenant_id, "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 + "folder_path": settings.onedrive_folder_path or "Documents/Uploads", "integration_id": integration_id, + "integration_name": None, + "integration_type": None, }, ) diff --git a/frontend/templates/dropbox.html b/frontend/templates/dropbox.html index 6c59b9e9..fb8fb469 100644 --- a/frontend/templates/dropbox.html +++ b/frontend/templates/dropbox.html @@ -4,6 +4,37 @@ {% block content %}
+ Authorize DocuElevate to access your Dropbox account. Your credentials are stored securely in your personal integration record. +
+ {% if is_configured %} +✓ Already authorized
+This integration already has credentials. You can re-authorize below to refresh or update them.
+ {% if folder_path %}
Folder: {{ folder_path }}{% endif %}
+
Authorization required
+Complete the wizard below to grant access to your Dropbox account.
+ {% if folder_path %}
Target folder: {{ folder_path }}{% endif %}
+
Configure the Dropbox integration for DocuElevate using our setup wizard. @@ -37,6 +68,7 @@
Enter the folder path where files should be uploaded (e.g., /Documents/Uploads)
Target folder (from integration settings)
+{{ folder_path }}
+@@ -163,6 +207,7 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}
Saving credentials to your integration...
'; diff --git a/frontend/templates/google_drive.html b/frontend/templates/google_drive.html index 3a964b04..ff0fc18e 100644 --- a/frontend/templates/google_drive.html +++ b/frontend/templates/google_drive.html @@ -4,6 +4,37 @@ {% block content %}+ Authorize DocuElevate to access your Google Drive account. Your credentials are stored securely in your personal integration record. +
+ {% if is_configured %} +✓ Already authorized
+This integration already has credentials. You can re-authorize below to refresh or update them.
+ {% if folder_id %}
Folder ID: {{ folder_id }}{% endif %}
+
Authorization required
+Complete the wizard below to grant access to your Google Drive account.
+ {% if folder_id %}
Target folder ID: {{ folder_id }}{% endif %}
+
Configure the Google Drive integration for DocuElevate using our setup wizard. @@ -42,8 +73,10 @@
Target folder ID (from integration settings)
+{{ folder_id }}
+@@ -307,6 +372,7 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}