fix: 100% coverage, WCAG accessibility fixes, and documentation updates for per-user OAuth wizards

Test coverage:
- Add config=None branch test for dropbox, onedrive, google_drive views (100% coverage)
- Add WATCH_FOLDER source-type test (folder_path vs folder key)
- Add integration-not-found fallback-to-admin-mode test

WCAG 2.1 AA fixes:
- Add aria-labelledby="modalTitle" to role="dialog" modals in setup templates
- Add aria-hidden="true" to decorative SVGs in callback templates
- Add role="status" aria-label="Loading" to spinner divs
- Add aria-live="polite" to processing-message and success/folder-selection regions
- Add role="alert" aria-live="assertive" to error containers
- Update "Return to Setup" link to preserve integration_id in user mode

Docs: update DropboxSetup.md, GoogleDriveSetup.md, OneDriveSetup.md with per-user OAuth flow section

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-09 20:19:19 +00:00
parent 2d01e71afd
commit 0529ae53ff
12 changed files with 259 additions and 58 deletions
+27 -13
View File
@@ -15,23 +15,37 @@ For a complete list of configuration options, see the [Configuration Guide](Conf
## Setup Methods ## 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 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. **Manual Setup**: Following the step-by-step instructions in this document 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 1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
2. Follow the on-screen instructions to create a Dropbox app 2. Create a Dropbox destination integration (or a Watch Folder with `source_type = dropbox`).
3. Enter your App Key and App Secret in the wizard 3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration.
4. Optionally specify a custom folder path for uploads 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" to begin the authorization process 5. Click **Start Authentication Flow**, authorize access in Dropbox, and the refresh token is automatically saved to your personal integration record.
6. Complete the Dropbox authentication process 6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
7. The system will automatically exchange the authorization code for a refresh token
8. Copy the generated environment variables for your worker nodes > **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. The wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment.
+16 -2
View File
@@ -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). 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: 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. 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. 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: The OAuth method is preferred as it:
- Provides better security with token expiration and refresh - Provides better security with token expiration and refresh
+26 -12
View File
@@ -16,22 +16,36 @@ For a complete list of configuration options, see the [Configuration Guide](Conf
## Setup Methods ## 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 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. **Manual Setup**: Following the step-by-step instructions in this document 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) 1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
2. Navigate to the OneDrive Setup page at `/onedrive-setup` 2. Create a OneDrive destination integration (or a Watch Folder with `source_type = onedrive`).
3. Enter your Client ID and other required information 3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration.
4. Click "Start Authentication Flow" 4. Enter your Azure AD Client ID and Client Secret in the wizard.
5. Complete the Microsoft authentication process 5. Click **Start Authentication Flow**, authorize access via Microsoft, and the refresh token is automatically saved to your personal integration record.
6. The system will automatically exchange the authorization code for a refresh token 6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
7. Copy the generated environment variables for your worker nodes
> **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. The auth wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment.
+1 -1
View File
@@ -241,7 +241,7 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
</div> </div>
<!-- Result Modal --> <!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog"> <div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="modalTitle">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white"> <div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
<div class="mt-3 text-center"> <div class="mt-3 text-center">
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4"> <div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
+16 -8
View File
@@ -5,7 +5,7 @@
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="bg-white shadow-md rounded-lg p-6"> <div class="bg-white shadow-md rounded-lg p-6">
<div class="text-center mb-6"> <div class="text-center mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2> <h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
@@ -13,18 +13,18 @@
</div> </div>
<div class="flex justify-center my-6"> <div class="flex justify-center my-6">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" role="status" aria-label="Loading"></div>
</div> </div>
<div id="processing-message" class="text-center text-gray-700"> <div id="processing-message" class="text-center text-gray-700" aria-live="polite">
<p>Exchanging authorization code for refresh token...</p> <p>Exchanging authorization code for refresh token...</p>
</div> </div>
<div id="error-container" class="hidden mt-6"> <div id="error-container" class="hidden mt-6" role="alert" aria-live="assertive">
<div class="bg-red-50 border-l-4 border-red-400 p-4"> <div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
@@ -37,17 +37,17 @@
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<a href="/dropbox-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> <a id="return-to-setup-link" href="/dropbox-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Return to Setup Return to Setup
</a> </a>
</div> </div>
</div> </div>
<div id="success-container" class="hidden mt-6"> <div id="success-container" class="hidden mt-6" aria-live="polite">
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
@@ -99,6 +99,14 @@ document.addEventListener('DOMContentLoaded', function() {
const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads'; const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads';
const integrationId = sessionStorage.getItem('oauth_integration_id'); const integrationId = sessionStorage.getItem('oauth_integration_id');
// In user mode, point the "Return to Setup" link back to the wizard with integration_id
if (integrationId) {
const returnLink = document.getElementById('return-to-setup-link');
if (returnLink) {
returnLink.href = `/dropbox-setup?integration_id=${encodeURIComponent(integrationId)}`;
}
}
const redirectUri = window.location.origin + "/dropbox-callback"; const redirectUri = window.location.origin + "/dropbox-callback";
// Automatically exchange the code for a refresh token // Automatically exchange the code for a refresh token
+1 -1
View File
@@ -404,7 +404,7 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
</div> </div>
<!-- Result Modal --> <!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog"> <div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="modalTitle">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white"> <div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
<div class="mt-3 text-center"> <div class="mt-3 text-center">
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4"> <div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
+19 -11
View File
@@ -5,7 +5,7 @@
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="bg-white shadow-md rounded-lg p-6"> <div class="bg-white shadow-md rounded-lg p-6">
<div class="text-center mb-6"> <div class="text-center mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2> <h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
@@ -13,18 +13,18 @@
</div> </div>
<div class="flex justify-center my-6"> <div class="flex justify-center my-6">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" role="status" aria-label="Loading"></div>
</div> </div>
<div id="processing-message" class="text-center text-gray-700"> <div id="processing-message" class="text-center text-gray-700" aria-live="polite">
<p>Exchanging authorization code for refresh token...</p> <p>Exchanging authorization code for refresh token...</p>
</div> </div>
<div id="error-container" class="hidden mt-6"> <div id="error-container" class="hidden mt-6" role="alert" aria-live="assertive">
<div class="bg-red-50 border-l-4 border-red-400 p-4"> <div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
@@ -37,17 +37,17 @@
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<a href="/google-drive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> <a id="return-to-setup-link" href="/google-drive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Return to Setup Return to Setup
</a> </a>
</div> </div>
</div> </div>
<div id="folder-selection-container" class="hidden mt-6"> <div id="folder-selection-container" class="hidden mt-6" aria-live="polite">
<div class="rounded-md bg-blue-50 p-4 mb-6"> <div class="rounded-md bg-blue-50 p-4 mb-6">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
@@ -65,7 +65,7 @@
<div class="mt-1 flex rounded-md shadow-sm"> <div class="mt-1 flex rounded-md shadow-sm">
<input type="text" id="folder-id-input" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" placeholder="Enter Google Drive folder ID"> <input type="text" id="folder-id-input" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" placeholder="Enter Google Drive folder ID">
<button id="folder-select-picker-btn" type="button" class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <button id="folder-select-picker-btn" type="button" class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="h-4 w-4 mr-1 text-white-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-4 w-4 mr-1 text-white-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" />
</svg> </svg>
Select Folder Select Folder
@@ -84,11 +84,11 @@
</div> </div>
</div> </div>
<div id="success-container" class="hidden mt-6"> <div id="success-container" class="hidden mt-6" aria-live="polite">
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
@@ -140,6 +140,14 @@ document.addEventListener('DOMContentLoaded', function() {
const folderId = sessionStorage.getItem('google_drive_folder_id'); const folderId = sessionStorage.getItem('google_drive_folder_id');
const integrationId = sessionStorage.getItem('oauth_integration_id'); const integrationId = sessionStorage.getItem('oauth_integration_id');
// In user mode, point the "Return to Setup" link back to the wizard with integration_id
if (integrationId) {
const returnLink = document.getElementById('return-to-setup-link');
if (returnLink) {
returnLink.href = `/google-drive-setup?integration_id=${encodeURIComponent(integrationId)}`;
}
}
const redirectUri = window.location.origin + "/google-drive-callback"; const redirectUri = window.location.origin + "/google-drive-callback";
let accessToken = null; let accessToken = null;
+1 -1
View File
@@ -262,7 +262,7 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
</div> </div>
<!-- Result Modal --> <!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog"> <div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="modalTitle">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white"> <div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
<div class="mt-3 text-center"> <div class="mt-3 text-center">
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4"> <div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
+16 -8
View File
@@ -5,7 +5,7 @@
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="bg-white shadow-md rounded-lg p-6"> <div class="bg-white shadow-md rounded-lg p-6">
<div class="text-center mb-6"> <div class="text-center mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2> <h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
@@ -13,18 +13,18 @@
</div> </div>
<div class="flex justify-center my-6"> <div class="flex justify-center my-6">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" role="status" aria-label="Loading"></div>
</div> </div>
<div id="processing-message" class="text-center text-gray-700"> <div id="processing-message" class="text-center text-gray-700" aria-live="polite">
<p>Exchanging authorization code for refresh token...</p> <p>Exchanging authorization code for refresh token...</p>
</div> </div>
<div id="error-container" class="hidden mt-6"> <div id="error-container" class="hidden mt-6" role="alert" aria-live="assertive">
<div class="bg-red-50 border-l-4 border-red-400 p-4"> <div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
@@ -37,17 +37,17 @@
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<a href="/onedrive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> <a id="return-to-setup-link" href="/onedrive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Return to Setup Return to Setup
</a> </a>
</div> </div>
</div> </div>
<div id="success-container" class="hidden mt-6"> <div id="success-container" class="hidden mt-6" aria-live="polite">
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
@@ -100,6 +100,14 @@ document.addEventListener('DOMContentLoaded', function() {
const folderPath = sessionStorage.getItem('onedrive_folder_path') || ""; const folderPath = sessionStorage.getItem('onedrive_folder_path') || "";
const integrationId = sessionStorage.getItem('oauth_integration_id'); const integrationId = sessionStorage.getItem('oauth_integration_id');
// In user mode, point the "Return to Setup" link back to the wizard with integration_id
if (integrationId) {
const returnLink = document.getElementById('return-to-setup-link');
if (returnLink) {
returnLink.href = `/onedrive-setup?integration_id=${encodeURIComponent(integrationId)}`;
}
}
const redirectUri = window.location.origin + "/onedrive-callback"; const redirectUri = window.location.origin + "/onedrive-callback";
// Automatically exchange the code for a refresh token // Automatically exchange the code for a refresh token
+52 -1
View File
@@ -72,7 +72,58 @@ class TestDropboxViews:
# Should render user mode without errors despite the bad config # Should render user mode without errors despite the bad config
assert b"user_mode" not in response.content or b"Back to Integrations" in response.content assert b"user_mode" not in response.content or b"Back to Integrations" in response.content
def test_dropbox_setup_user_mode_valid_config(self, client, db_session): def test_dropbox_setup_user_mode_none_config(self, client, db_session):
"""Test user-mode renders correctly when integration.config is None (no folder path)."""
owner_id = "user_none_cfg@example.com"
integration = UserIntegration(
owner_id=owner_id,
direction="DESTINATION",
integration_type="DROPBOX",
name="My Dropbox (no cfg)",
config=None,
is_active=True,
)
db_session.add(integration)
db_session.commit()
db_session.refresh(integration)
with patch("app.views.dropbox.get_current_owner_id", return_value=owner_id):
response = client.get(f"/dropbox-setup?integration_id={integration.id}")
assert response.status_code == 200
# Should render user mode without errors, with empty folder_path
assert b"Back to Integrations" in response.content
def test_dropbox_setup_user_mode_watchfolder_config(self, client, db_session):
"""Test user-mode correctly loads folder_path from WATCH_FOLDER source config."""
owner_id = "user_wf_cfg@example.com"
integration = UserIntegration(
owner_id=owner_id,
direction="SOURCE",
integration_type="WATCH_FOLDER",
name="My Dropbox Watch",
config=json.dumps({"source_type": "dropbox", "folder_path": "/Inbox"}),
is_active=True,
)
db_session.add(integration)
db_session.commit()
db_session.refresh(integration)
with patch("app.views.dropbox.get_current_owner_id", return_value=owner_id):
response = client.get(f"/dropbox-setup?integration_id={integration.id}")
assert response.status_code == 200
assert b"/Inbox" in response.content
assert b"Back to Integrations" in response.content
def test_dropbox_setup_user_mode_integration_not_found(self, client, db_session):
"""Test user-mode falls back to admin mode when integration not owned by user."""
with patch("app.views.dropbox.get_current_owner_id", return_value="other_user@example.com"):
response = client.get("/dropbox-setup?integration_id=999999")
assert response.status_code == 200
# Falls back to admin mode (no "Back to Integrations" link)
assert b"Dropbox Integration Setup" in response.content
"""Test user-mode correctly loads folder path from integration config.""" """Test user-mode correctly loads folder path from integration config."""
owner_id = "user_valid_cfg@example.com" owner_id = "user_valid_cfg@example.com"
integration = UserIntegration( integration = UserIntegration(
+31
View File
@@ -182,6 +182,37 @@ class TestGoogleDriveViews:
# Should render user mode without errors despite the bad config # Should render user mode without errors despite the bad config
assert b"Back to Integrations" in response.content assert b"Back to Integrations" in response.content
def test_google_drive_setup_user_mode_none_config(self, client, db_session):
"""Test user-mode renders correctly when integration.config is None (no folder ID)."""
owner_id = "user_gd_none_cfg@example.com"
integration = UserIntegration(
owner_id=owner_id,
direction="DESTINATION",
integration_type="GOOGLE_DRIVE",
name="My GDrive (no cfg)",
config=None,
is_active=True,
)
db_session.add(integration)
db_session.commit()
db_session.refresh(integration)
with patch("app.views.google_drive.get_current_owner_id", return_value=owner_id):
response = client.get(f"/google-drive-setup?integration_id={integration.id}")
assert response.status_code == 200
# Should render user mode without errors, with empty folder_id
assert b"Back to Integrations" in response.content
def test_google_drive_setup_user_mode_integration_not_found(self, client, db_session):
"""Test user-mode falls back to admin mode when integration not owned by user."""
with patch("app.views.google_drive.get_current_owner_id", return_value="other_user@example.com"):
response = client.get("/google-drive-setup?integration_id=999999")
assert response.status_code == 200
# Falls back to admin mode (no "Back to Integrations" link)
assert b"Google Drive Integration Setup" in response.content
def test_google_drive_setup_user_mode_valid_config(self, client, db_session): def test_google_drive_setup_user_mode_valid_config(self, client, db_session):
"""Test user-mode correctly loads folder ID from integration config.""" """Test user-mode correctly loads folder ID from integration config."""
owner_id = "user_gd_valid_cfg@example.com" owner_id = "user_gd_valid_cfg@example.com"
+53
View File
@@ -70,6 +70,59 @@ class TestOnedriveViews:
# Should render user mode without errors despite the bad config # Should render user mode without errors despite the bad config
assert b"Back to Integrations" in response.content assert b"Back to Integrations" in response.content
def test_onedrive_setup_user_mode_none_config(self, client, db_session):
"""Test user-mode renders correctly when integration.config is None (no folder path)."""
owner_id = "user_od_none_cfg@example.com"
integration = UserIntegration(
owner_id=owner_id,
direction="DESTINATION",
integration_type="ONEDRIVE",
name="My OneDrive (no cfg)",
config=None,
is_active=True,
)
db_session.add(integration)
db_session.commit()
db_session.refresh(integration)
with patch("app.views.onedrive.get_current_owner_id", return_value=owner_id):
response = client.get(f"/onedrive-setup?integration_id={integration.id}")
assert response.status_code == 200
# Should render user mode without errors, with empty folder_path
assert b"Back to Integrations" in response.content
def test_onedrive_setup_user_mode_watchfolder_config(self, client, db_session):
"""Test user-mode correctly loads folder_path from WATCH_FOLDER source config."""
owner_id = "user_od_wf_cfg@example.com"
integration = UserIntegration(
owner_id=owner_id,
direction="SOURCE",
integration_type="WATCH_FOLDER",
name="My OneDrive Watch",
config=json.dumps({"source_type": "onedrive", "folder_path": "Work/Inbox"}),
is_active=True,
)
db_session.add(integration)
db_session.commit()
db_session.refresh(integration)
with patch("app.views.onedrive.get_current_owner_id", return_value=owner_id):
response = client.get(f"/onedrive-setup?integration_id={integration.id}")
assert response.status_code == 200
assert b"Work/Inbox" in response.content
assert b"Back to Integrations" in response.content
def test_onedrive_setup_user_mode_integration_not_found(self, client, db_session):
"""Test user-mode falls back to admin mode when integration not owned by user."""
with patch("app.views.onedrive.get_current_owner_id", return_value="other_user@example.com"):
response = client.get("/onedrive-setup?integration_id=999999")
assert response.status_code == 200
# Falls back to admin mode (no "Back to Integrations" link)
assert b"OneDrive Integration Setup" in response.content
def test_onedrive_setup_user_mode_valid_config(self, client, db_session): def test_onedrive_setup_user_mode_valid_config(self, client, db_session):
"""Test user-mode correctly loads folder path from integration config.""" """Test user-mode correctly loads folder path from integration config."""
owner_id = "user_od_valid_cfg@example.com" owner_id = "user_od_valid_cfg@example.com"