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 %}
+ {% if user_mode %} + + +

+ Connect Dropbox + {% if integration_name %}— {{ integration_name }}{% endif %} +

+

+ Authorize DocuElevate to access your Dropbox account. Your credentials are stored securely in your personal integration record. +

+ {% if is_configured %} + + {% else %} + + {% endif %} + {% else %} +

Dropbox Integration Setup

Configure the Dropbox integration for DocuElevate using our setup wizard. @@ -37,6 +68,7 @@

+ {% endif %}
@@ -79,24 +111,35 @@
-

Complete Setup with Wizard

+

+ {% if user_mode %}OAuth Wizard{% else %}Complete Setup with Wizard{% endif %} +

- +
- +
+ {% if not user_mode %}

Enter the folder path where files should be uploaded (e.g., /Documents/Uploads)

+ {% else %} + {% if folder_path %} +
+

Target folder (from integration settings)

+

{{ folder_path }}

+
+ {% endif %} + {% endif %}
- + + {% if not user_mode %}
@@ -140,7 +184,7 @@
- +

Configuration for Worker Nodes

@@ -163,6 +207,7 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}

+ {% endif %}
@@ -184,9 +229,15 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}
+ {% if user_mode %} + + Back to Integrations + + {% else %} Back to Status + {% endif %}
@@ -216,6 +267,8 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }} document.addEventListener('DOMContentLoaded', function() { + const userMode = {{ 'true' if user_mode else 'false' }}; + // Store integration_id if provided (for per-user OAuth flow) const integrationId = "{{ integration_id or '' }}"; if (integrationId) { @@ -279,7 +332,6 @@ document.addEventListener('DOMContentLoaded', function() { startAuthFlowBtn.addEventListener('click', function() { const appKey = document.getElementById('app-key').value.trim(); const appSecret = appSecretInput.value.trim(); - const folderPath = document.getElementById('folder-path').value.trim(); const redirectUri = window.location.origin + "/dropbox-callback"; if (!appKey) { @@ -292,11 +344,16 @@ document.addEventListener('DOMContentLoaded', function() { return; } - // Save app key, app secret and folder path to session storage temporarily + // Save app key and app secret to session storage temporarily sessionStorage.setItem('dropbox_app_key', appKey); sessionStorage.setItem('dropbox_app_secret', appSecret); - if (folderPath) { - sessionStorage.setItem('dropbox_folder_path', folderPath); + + // In admin mode also store folder path; in user mode the config is already set + if (!userMode) { + const folderPathEl = document.getElementById('folder-path'); + if (folderPathEl && folderPathEl.value.trim()) { + sessionStorage.setItem('dropbox_folder_path', folderPathEl.value.trim()); + } } // Generate the authorization URL @@ -306,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() { window.location.href = authUrl; }); - // Test Token button click + // Test Token button click (admin mode only) if (testTokenBtn) { testTokenBtn.addEventListener('click', function() { testTokenBtn.innerHTML = ' Testing...'; @@ -343,7 +400,7 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Refresh Token button click + // Refresh Token button click (admin mode only) if (refreshTokenBtn) { refreshTokenBtn.addEventListener('click', function() { showModal('info', 'Confirm', 'This will start a new authentication flow to obtain a fresh token from Dropbox. Continue?'); @@ -374,7 +431,7 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Copy Environment Variables Button + // Copy Environment Variables Button (admin mode only) const copyEnvVarsBtn = document.getElementById('copy-env-vars'); if (copyEnvVarsBtn) { copyEnvVarsBtn.addEventListener('click', function() { @@ -403,8 +460,8 @@ document.addEventListener('DOMContentLoaded', function() { sessionStorage.removeItem('dropbox_app_secret'); } - // If token is not configured but we have an app key, show the token status section - if (document.getElementById('app-key').value && !tokenStatus.classList.contains('hidden')) { + // If token is not configured but we have an app key, show the token status section (admin mode) + if (tokenStatus && document.getElementById('app-key').value && !tokenStatus.classList.contains('hidden')) { tokenStatus.classList.remove('hidden'); } }); diff --git a/frontend/templates/dropbox_callback.html b/frontend/templates/dropbox_callback.html index 5d253624..7d9f0e1e 100644 --- a/frontend/templates/dropbox_callback.html +++ b/frontend/templates/dropbox_callback.html @@ -145,11 +145,10 @@ document.addEventListener('DOMContentLoaded', function() { app_key: appKey, app_secret: appSecret, }; - const cfgUpdate = {}; - if (folderPath) cfgUpdate.folder = folderPath; + // Only send credentials — the integration's config (folder, source_type, etc.) + // is already set and must not be overwritten here. const body = { credentials: creds }; - if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate; document.getElementById('processing-message').innerHTML = '

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 %}
+ {% if user_mode %} + + +

+ Connect Google Drive + {% if integration_name %}— {{ integration_name }}{% endif %} +

+

+ Authorize DocuElevate to access your Google Drive account. Your credentials are stored securely in your personal integration record. +

+ {% if is_configured %} + + {% else %} + + {% endif %} + {% else %} +

Google Drive Integration Setup

Configure the Google Drive integration for DocuElevate using our setup wizard. @@ -42,8 +73,10 @@

+ {% endif %} + {% if not user_mode %}

Authentication Method

@@ -224,7 +257,39 @@
- + + {% endif %}{# end if not user_mode for admin auth-method block #} + + + {% if user_mode %} +
+

OAuth Wizard

+
+
+ + +
+
+ + +
+ {% if folder_id %} +
+

Target folder ID (from integration settings)

+

{{ folder_id }}

+
+ {% endif %} +
+ +
+
+
+ {% endif %} + + + {% if not user_mode %}

Connection Status

@@ -262,7 +327,7 @@
- +

OAuth Configuration for Worker Nodes

@@ -307,6 +372,7 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}

+ {% endif %}
@@ -326,9 +392,15 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
+ {% if user_mode %} + + Back to Integrations + + {% else %} Back to Status + {% endif %}
@@ -358,18 +430,14 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }} {% block scripts %}