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/docs/DropboxSetup.md b/docs/DropboxSetup.md index 37de4701..8923a595 100644 --- a/docs/DropboxSetup.md +++ b/docs/DropboxSetup.md @@ -15,23 +15,37 @@ For a complete list of configuration options, see the [Configuration Guide](Conf ## Setup Methods -You can set up Dropbox integration in two ways: +DocuElevate supports two distinct Dropbox OAuth flows: -1. **Using the Built-in Setup Wizard (Recommended)**: An interactive setup experience available at `/dropbox-setup` in the web interface -2. **Manual Setup**: Following the step-by-step instructions in this document +1. **Per-User Integration Wizard (Recommended for end users)**: Triggered from the Integrations dashboard (`/integrations`) by clicking **Authorize** on a Dropbox destination or Dropbox-backed Watch Folder. Credentials are saved securely to your personal integration record — global settings are never exposed. +2. **System-Level Setup Wizard**: Available at `/dropbox-setup` for administrators configuring the global system-wide Dropbox connection. Generates environment variables for all worker nodes. +3. **Manual Setup**: Following the step-by-step instructions in this document. -## Using the Setup Wizard +## Per-User OAuth Flow (Integrations Dashboard) -The easiest way to set up Dropbox integration is to use the built-in setup wizard: +End users authorize their own Dropbox integration from the **Integrations** dashboard: -1. Navigate to the `/dropbox-setup` page in your DocuElevate instance -2. Follow the on-screen instructions to create a Dropbox app -3. Enter your App Key and App Secret in the wizard -4. Optionally specify a custom folder path for uploads -5. Click "Start Authentication Flow" to begin the authorization process -6. Complete the Dropbox authentication process -7. The system will automatically exchange the authorization code for a refresh token -8. Copy the generated environment variables for your worker nodes +1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder). +2. Create a Dropbox destination integration (or a Watch Folder with `source_type = dropbox`). +3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration. +4. Enter your Dropbox App Key and App Secret in the wizard (or use the global admin credentials if pre-configured). +5. Click **Start Authentication Flow**, authorize access in Dropbox, and the refresh token is automatically saved to your personal integration record. +6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button. + +> **Note:** Your credentials are stored encrypted per-integration and are never mixed with other users' data. Each user can have multiple Dropbox integrations with independent tokens. + +## Using the System-Level Setup Wizard (Admin) + +The easiest way to configure the global Dropbox integration is to use the built-in setup wizard: + +1. Navigate to the `/dropbox-setup` page in your DocuElevate instance. +2. Follow the on-screen instructions to create a Dropbox app. +3. Enter your App Key and App Secret in the wizard. +4. Optionally specify a custom folder path for uploads. +5. Click **Start Authentication Flow** to begin the authorization process. +6. Complete the Dropbox authentication process. +7. The system will automatically exchange the authorization code for a refresh token. +8. Copy the generated environment variables for your worker nodes. The wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment. diff --git a/docs/GoogleDriveSetup.md b/docs/GoogleDriveSetup.md index 73107077..e85addcc 100644 --- a/docs/GoogleDriveSetup.md +++ b/docs/GoogleDriveSetup.md @@ -16,14 +16,28 @@ This guide explains how to set up the Google Drive integration for DocuElevate. For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md). -## Authentication Methods +## Authentication Methods and Setup Flows DocuElevate supports two authentication methods for Google Drive: 1. **OAuth Authentication (Recommended)** - User-based authentication that provides better security and control. Recommended for most deployments. 2. **Service Account Authentication** - Server-to-server authentication that doesn't require user interaction. Useful for specific enterprise deployments. -## Method 1: OAuth Authentication Setup (Recommended) +### Per-User OAuth Flow (Integrations Dashboard) + +End users can authorize their own Google Drive integration directly from the **Integrations** dashboard — no admin involvement required: + +1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder). +2. Create a Google Drive destination integration (or a Watch Folder with `source_type = google_drive`). +3. Click the **Authorize** button — it opens the OAuth wizard pre-loaded with your integration's configuration. +4. Enter your Google OAuth Client ID and Client Secret in the wizard. +5. Click **Start Authentication Flow** and authorize access in Google. +6. Credentials are saved automatically to your personal integration record; the page redirects back to `/integrations`. +7. Re-authorization is available at any time via the **Re-Authorize** button. + +> **Note:** Credentials are stored encrypted per-integration. Each user can hold multiple Google Drive integrations with independent tokens targeting different folders or accounts. + +## Method 1: OAuth Authentication Setup (System-Level) The OAuth method is preferred as it: - Provides better security with token expiration and refresh diff --git a/docs/OneDriveSetup.md b/docs/OneDriveSetup.md index 035fb61e..e9a555f4 100644 --- a/docs/OneDriveSetup.md +++ b/docs/OneDriveSetup.md @@ -16,22 +16,36 @@ For a complete list of configuration options, see the [Configuration Guide](Conf ## Setup Methods -You can set up OneDrive integration in two ways: +DocuElevate supports two distinct OneDrive OAuth flows: -1. **Using the Auth Wizard (Recommended)**: An interactive setup experience available at `/onedrive-setup` in the web interface -2. **Manual Setup**: Following the step-by-step instructions in this document +1. **Per-User Integration Wizard (Recommended for end users)**: Triggered from the Integrations dashboard (`/integrations`) by clicking **Authorize** on a OneDrive destination or OneDrive-backed Watch Folder. Credentials are saved securely to your personal integration record. +2. **System-Level Setup Wizard**: Available at `/onedrive-setup` for administrators configuring the global system-wide OneDrive connection. +3. **Manual Setup**: Following the step-by-step instructions in this document. -## Using the Auth Wizard +## Per-User OAuth Flow (Integrations Dashboard) -The easiest way to set up OneDrive integration is to use the built-in auth wizard: +End users authorize their own OneDrive integration from the **Integrations** dashboard: -1. Register an application in Azure AD (see steps below) -2. Navigate to the OneDrive Setup page at `/onedrive-setup` -3. Enter your Client ID and other required information -4. Click "Start Authentication Flow" -5. Complete the Microsoft authentication process -6. The system will automatically exchange the authorization code for a refresh token -7. Copy the generated environment variables for your worker nodes +1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder). +2. Create a OneDrive destination integration (or a Watch Folder with `source_type = onedrive`). +3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration. +4. Enter your Azure AD Client ID and Client Secret in the wizard. +5. Click **Start Authentication Flow**, authorize access via Microsoft, and the refresh token is automatically saved to your personal integration record. +6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button. + +> **Note:** Your credentials are stored encrypted per-integration and are never mixed with other users' data. Each user can have multiple OneDrive integrations with independent tokens. + +## Using the System-Level Auth Wizard (Admin) + +The easiest way to configure the global OneDrive integration is to use the built-in auth wizard: + +1. Register an application in Azure AD (see steps below). +2. Navigate to the OneDrive Setup page at `/onedrive-setup`. +3. Enter your Client ID and other required information. +4. Click **Start Authentication Flow**. +5. Complete the Microsoft authentication process. +6. The system will automatically exchange the authorization code for a refresh token. +7. Copy the generated environment variables for your worker nodes. The auth wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment. diff --git a/frontend/templates/dropbox.html b/frontend/templates/dropbox.html index 6c59b9e9..a94a5b96 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,13 +229,19 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}
+ {% if user_mode %} + + Back to Integrations + + {% else %} Back to Status + {% endif %}
-