Files
gh-christianlouis-docuelevate/frontend/templates/google_drive.html
T
copilot-swe-agent[bot] c7d3ec57c3 fix: restore all code deleted/truncated by d2217531 Jules SSRF commit
Commit d2217531 (google-labs-jules SSRF fix) catastrophically deleted
11,500+ lines across 100+ files while fixing an unrelated IMAP issue.

Restored from d2217531^ (pre-bad-commit state):

Deleted files (fully restored):
- app/api/{automation,classification_rules,comments,sharing}.py
- app/middleware/upload_rate_limit.py
- app/tasks/{automation_tasks,classify_document}.py
- app/utils/{automation_hooks,classification_rules}.py
- docs/AppleAppStoreCompliance.md
- frontend/input.css, package.json, package-lock.json, tailwind.config.js
- frontend/static/js/{annotations,claim,comments,sharing}.js
- frontend/templates/{admin_connections,file_annotations,file_summary}.html
- tests/{test_api_files_comprehensive,test_auth_extended,test_sharing,
         test_comments,test_connections,test_imap_profiles,test_api_sessions,
         test_automation,test_classification_rules,test_api_advanced_filters,
         test_api_classification_rules,test_upload_rate_limit,test_api_dropbox,
         test_classify_document,test_comments_ui,test_upload_to_icloud,
         test_api_onedrive_comprehensive,test_frontend_build,test_sentry,
         test_diagnostic,test_database,test_views_dropbox,test_local_auth}.py

Truncated files (content restored):
- app/{auth,config,main,models,celery_worker,database}.py
- app/api/{__init__,api_tokens,diagnostic,dropbox,files,google_drive,
           integrations,local_auth,mobile,onedrive,pipelines,qr_auth,
           settings,url_upload}.py
- app/middleware/upload_rate_limit.py
- app/tasks/upload_to_nextcloud.py
- app/utils/{allowed_types,settings_service,settings_sync,user_scope,webhook}.py
- app/views/{base,dropbox,files,google_drive,onedrive,settings}.py
- docs/{API,AuthenticationSetup,ConfigurationGuide,DatabaseConfiguration,
        DeploymentGuide,DropboxSetup,GoogleDriveSetup,KubernetesDeployment,
        MobileApp,OneDriveSetup,ProductionReadiness,SentrySetup,
        SocialLoginSetup,UserGuide}.md
- frontend/static/{js/upload.js,styles.css}
- frontend/templates/{api_tokens,base,devices,dropbox,dropbox_callback,
                      file_view,files,google_drive,onedrive,onedrive_callback,
                      signup}.html
- frontend/translations/en.json
- migrations/env.py
- tests/{conftest,test_api_integrations,test_api_mobile,test_api_settings,
         test_api_tokens,test_audit_logs,test_duplicates,test_imap_tasks,
         test_setup_wizard,test_views_files_comprehensive}.py

Security fixes kept from post-d2217531 commits:
- app/utils/network.py: DNS SSRF fail-secure fix (06b0fced)
- app/utils/file_operations.py: path traversal fix (1018ea17)
- tests/test_imap_tasks.py: re-applied 4 is_private_ip mock patches

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/51133dd8-9bec-41ab-aa10-3de753634187
2026-03-23 23:52:39 +00:00

960 lines
43 KiB
HTML

{% extends "base.html" %}
{% block title %}Google Drive Setup{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
{% if user_mode %}
<!-- ── User-mode header ──────────────────────────────────────────── -->
<div class="mb-4">
<a href="/integrations" class="inline-flex items-center text-sm text-indigo-600 hover:text-indigo-800">
<i class="fas fa-arrow-left mr-1" aria-hidden="true"></i> Back to Integrations
</a>
</div>
<h1 class="text-3xl font-bold mb-2">
Connect Google Drive
{% if integration_name %}<span class="text-gray-500 text-2xl font-normal">— {{ integration_name }}</span>{% endif %}
</h1>
<p class="text-gray-600 mb-4">
Authorize DocuElevate to access your Google Drive account. Your credentials are stored securely in your personal integration record.
</p>
{% if is_configured %}
<div class="bg-green-50 border-l-4 border-green-500 text-green-700 p-4 my-4" role="alert">
<p class="font-semibold">✓ Already authorized</p>
<p class="text-sm mt-1">This integration already has credentials. You can re-authorize below to refresh or update them.
{% if folder_id %}<br><strong>Folder ID:</strong> {{ folder_id }}{% endif %}
</p>
</div>
{% else %}
<div class="bg-blue-50 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
<p class="font-semibold">Authorization required</p>
<p class="text-sm mt-1">Complete the wizard below to grant access to your Google Drive account.
{% if folder_id %}<br><strong>Target folder ID:</strong> {{ folder_id }}{% endif %}
</p>
</div>
{% endif %}
{% else %}
<!-- ── Admin-mode header ─────────────────────────────────────────── -->
<h1 class="text-3xl font-bold mb-2">Google Drive Integration Setup</h1>
<p class="text-gray-600 mb-4">
Configure the Google Drive integration for DocuElevate using our setup wizard.
</p>
<div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
<p class="font-bold">Current Status:</p>
<p>Google Drive integration is
{% if is_configured %}
<span class="text-green-700 font-semibold">configured</span>.
{% else %}
<span class="text-red-700 font-semibold">not configured</span>.
{% endif %}
</p>
{% if use_oauth %}
<p class="mt-2"><strong>Authentication Type:</strong> OAuth Flow (User Account)</p>
{% else %}
<p class="mt-2"><strong>Authentication Type:</strong> Service Account (Server-to-Server)</p>
{% endif %}
{% if folder_id %}
<p class="mt-2"><strong>Target folder ID:</strong> {{ folder_id }}</p>
{% endif %}
</div>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-700">
<strong>Note:</strong> There are two ways to authenticate with Google Drive. This wizard will guide you through OAuth setup, which is recommended for most users. For service account setup, please refer to the documentation.
</p>
</div>
</div>
</div>
{% endif %}
</div>
{% if not user_mode %}
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<div class="flex justify-between items-start mb-4">
<h2 class="text-2xl font-semibold">Authentication Method</h2>
{% if is_configured %}
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Configured
</span>
{% else %}
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
Not Configured
</span>
{% endif %}
</div>
<div class="flex space-x-4 mb-6">
<button id="oauth-tab-btn" class="px-4 py-2 rounded-md {{ 'text-white bg-blue-600' if use_oauth else 'text-gray-700 bg-gray-200' }}">
OAuth User Account
</button>
<button id="sa-tab-btn" class="px-4 py-2 rounded-md {{ 'text-white bg-blue-600' if not use_oauth else 'text-gray-700 bg-gray-200' }}">
Service Account
</button>
</div>
<!-- OAuth Tab Content -->
<div id="oauth-tab" class="{{ 'block' if use_oauth else 'hidden' }}">
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 1: Create a Google Cloud Project</h3>
<ol class="list-decimal ml-6 space-y-3">
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="text-blue-600 hover:underline">Google Cloud Console</a></li>
<li>Create a new project or select an existing one</li>
<li>Make note of your project ID</li>
</ol>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 2: Enable the Google Drive API</h3>
<ol class="list-decimal ml-6 space-y-3">
<li>In your project, go to "APIs & Services" > "Library"</li>
<li>Search for "Google Drive API" and select it</li>
<li>Click the "Enable" button</li>
</ol>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 3: Configure OAuth Consent Screen</h3>
<ol class="list-decimal ml-6 space-y-3">
<li>In "APIs & Services", go to "OAuth consent screen"</li>
<li>Select "External" user type (or "Internal" if this is for an organization)</li>
<li>Fill out the required application information:
<ul class="list-disc ml-6 mt-1">
<li>App name: "DocuElevate" (or your preferred name)</li>
<li>User support email: Your email</li>
<li>Application homepage link: Your application URL</li>
<li>Authorized domains: Your domain</li>
<li>Developer contact information: Your email</li>
</ul>
</li>
<li>Click "Save and Continue"</li>
<li>On the Scopes screen, click "Add or Remove Scopes" and add:
<ul class="list-disc ml-6 mt-1">
<li><code class="bg-gray-100 p-1">https://www.googleapis.com/auth/drive.file</code></li>
</ul>
</li>
<li>Click "Save and Continue" through the rest of the setup</li>
</ol>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 4: Create OAuth Credentials</h3>
<ol class="list-decimal ml-6 space-y-3">
<li>Go to "APIs & Services" > "Credentials"</li>
<li>Click "Create Credentials" > "OAuth client ID"</li>
<li>Select "Web application" for Application type</li>
<li>Add a name (e.g., "DocuElevate Web Client")</li>
<li>Under "Authorized redirect URIs" add:
<div class="bg-gray-100 p-2 my-2 rounded text-sm">
{{ request.url.scheme }}://{{ request.url.netloc }}/google-drive-callback
</div>
</li>
<li>Click "Create"</li>
<li>Copy your Client ID and Client Secret</li>
</ol>
</div>
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 5: Complete OAuth Setup</h3>
<p class="mb-4">Now enter your Client ID and Client Secret below, and we'll help you complete the OAuth flow. You can set the folder ID after authentication is complete.</p>
<div class="space-y-4">
{% if user_mode and has_system_credentials %}
<!-- System credentials toggle (user mode only) -->
<div class="bg-green-50 border border-green-200 rounded-md p-4">
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" id="use-system-creds" checked class="rounded border-gray-300 text-green-600 focus:ring-green-500 h-5 w-5" />
<div>
<span class="text-sm font-medium text-green-800">Use DocuElevate's app credentials</span>
<p class="text-xs text-green-600 mt-0.5">Recommended — authorize with your Google account without needing your own Google Cloud app registration.</p>
</div>
</label>
</div>
{% endif %}
<div id="custom-creds-section" {% if user_mode and has_system_credentials %}class="hidden"{% endif %}>
<div class="space-y-4">
<div>
<label for="client-id" class="block text-sm font-medium text-gray-700">Client ID</label>
<input type="text" id="client-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client ID" value="{{ client_id_value if not (user_mode and has_system_credentials) else '' }}">
</div>
<div>
<label for="client-secret" class="block text-sm font-medium text-gray-700">Client Secret</label>
<input type="password" id="client-secret" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client Secret" value="{{ client_secret_value if not (user_mode and has_system_credentials) else '' }}">
</div>
</div>
</div>
<div>
<label for="folder-id" class="block text-sm font-medium text-gray-700">Folder ID (Optional)</label>
<input type="text" id="folder-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Google Drive folder ID" value="{{ folder_id }}">
<p class="text-xs text-gray-500 mt-1">Optional: You can set this after authentication if you prefer</p>
</div>
<div>
<button id="select-folder-btn" class="mb-2 inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="h-5 w-5 mr-2 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<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 d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z" />
</svg>
Select Folder with Picker
</button>
</div>
<div>
<button id="start-oauth-flow" 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">
Start Authentication Flow
</button>
</div>
</div>
</div>
</div>
<!-- Service Account Tab Content -->
<div id="sa-tab" class="{{ 'block' if not use_oauth else 'hidden' }}">
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Service Account Configuration</h3>
<p class="mb-4">Service accounts allow for server-to-server authentication without user involvement. This method is useful for background tasks.</p>
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
<div class="flex">
<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">
<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>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
Service account configuration requires creating a service account in Google Cloud Console and uploading credentials. For detailed instructions, refer to the documentation.
</p>
</div>
</div>
</div>
<div class="space-y-4">
<div>
<label for="sa-folder-id" class="block text-sm font-medium text-gray-700">Folder ID</label>
<input type="text" id="sa-folder-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Google Drive folder ID" value="{{ folder_id }}">
<p class="text-xs text-gray-500 mt-1">This is the ID from the Google Drive folder URL where files will be saved</p>
</div>
<div>
<label for="sa-credentials" class="block text-sm font-medium text-gray-700">Service Account Credentials</label>
<div class="mt-1">
<p class="text-xs text-gray-500 mb-2">
{% if has_credentials_json %}
Service account credentials are configured in your .env file.
{% else %}
No service account credentials found. You need to manually add them to your .env file.
{% endif %}
</p>
<a href="/docs/GoogleDriveServiceAccount.md" target="_blank" class="text-blue-600 hover:underline text-sm">
View Service Account Setup Documentation
</a>
</div>
</div>
<div>
<button id="save-sa-settings" 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">
Save Service Account Settings
</button>
</div>
</div>
</div>
</div>
</div>
{% endif %}{# end if not user_mode for admin auth-method block #}
<!-- ── User-mode OAuth wizard ────────────────────────────────────────── -->
{% if user_mode %}
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">OAuth Wizard</h2>
<div class="space-y-4">
<div>
<label for="client-id" class="block text-sm font-medium text-gray-700">Client ID <span class="text-red-500" aria-hidden="true">*</span></label>
<input type="text" id="client-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client ID" value="{{ client_id_value }}">
</div>
<div>
<label for="client-secret" class="block text-sm font-medium text-gray-700">Client Secret <span class="text-red-500" aria-hidden="true">*</span></label>
<input type="password" id="client-secret" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client Secret" value="{{ client_secret_value }}">
</div>
{% if folder_id %}
<div class="bg-gray-50 border border-gray-200 rounded-md px-3 py-2">
<p class="text-xs text-gray-500">Target folder ID (from integration settings)</p>
<p class="text-sm font-mono text-gray-700">{{ folder_id }}</p>
</div>
{% endif %}
<div>
<button id="start-oauth-flow" 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">
Start Authentication Flow
</button>
</div>
</div>
</div>
{% endif %}
<!-- Token validation and status (admin mode only) -->
{% if not user_mode %}
<div id="token-status" class="mt-6 bg-white shadow-md rounded-lg p-6 {{ 'hidden' if not is_configured else '' }}">
<h2 class="text-xl font-semibold mb-4">Connection Status</h2>
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
<div class="flex">
<div class="flex-shrink-0">
{% if is_configured %}
<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" />
</svg>
{% else %}
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
{% endif %}
</div>
<div class="ml-3">
<p class="text-sm font-medium {{ 'text-green-800' if is_configured else 'text-yellow-800' }}">
{% if is_configured %}
Google Drive integration is properly configured! Your credentials are valid.
{% else %}
Google Drive integration is not completely configured or credentials may be invalid.
{% endif %}
</p>
</div>
</div>
</div>
<div class="mt-4 flex space-x-3">
<button id="test-connection" class="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">
Test Connection
</button>
<button id="refresh-token-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Refresh Token
</button>
</div>
<!-- Configuration for Worker Nodes section (admin only) -->
<div id="oauth-env-section" class="mt-6 p-4 bg-gray-100 rounded-md {{ 'hidden' if not use_oauth else '' }}">
<h3 class="font-medium text-lg mb-2">OAuth Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="oauth-env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>GOOGLE_DRIVE_USE_OAUTH=true
GOOGLE_DRIVE_CLIENT_ID={{ client_id_value }}
GOOGLE_DRIVE_CLIENT_SECRET={{ client_secret_value|default('YOUR_CLIENT_SECRET', true) }}
GOOGLE_DRIVE_REFRESH_TOKEN={{ refresh_token_value|default('YOUR_REFRESH_TOKEN', true) }}
GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}</code></pre>
<button id="copy-oauth-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file or environment configuration.
</p>
</div>
<div id="sa-env-section" class="mt-6 p-4 bg-gray-100 rounded-md {{ 'hidden' if use_oauth else '' }}">
<h3 class="font-medium text-lg mb-2">Service Account Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="sa-env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>GOOGLE_DRIVE_USE_OAUTH=false
GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
# Make sure to add your GOOGLE_DRIVE_CREDENTIALS_JSON to your .env file</code></pre>
<button id="copy-sa-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file along with your service account credentials JSON.
</p>
</div>
</div>
{% endif %}
<div class="bg-white shadow-md rounded-lg p-6 mt-8 mb-8">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-lg font-medium">Need More Information?</h3>
<p class="mt-1 text-sm text-gray-600">
For detailed instructions and troubleshooting, refer to the
<a href="/docs/GoogleDriveSetup.md" target="_blank" class="text-blue-600 hover:underline">Google Drive Setup Documentation</a>.
</p>
</div>
</div>
</div>
<div class="mt-8">
{% if user_mode %}
<a href="/integrations" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fas fa-arrow-left mr-2" aria-hidden="true"></i> Back to Integrations
</a>
{% else %}
<a href="/status" 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">
Back to Status
</a>
{% endif %}
</div>
<!-- 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" 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="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">
<!-- Icon will be injected by JS -->
</div>
<h3 id="modalTitle" class="text-lg leading-6 font-medium text-gray-900">Success</h3>
<div class="mt-2 px-7 py-3">
<p id="modalMessage" class="text-sm text-gray-500">
Operation completed successfully.
</p>
</div>
<div class="items-center px-4 py-3">
<button id="modalClose" class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-300">
Close
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const userMode = {{ 'true' if user_mode else 'false' }};
const hasSystemCredentials = {{ 'true' if (user_mode and has_system_credentials) else 'false' }};
const systemClientId = {{ (client_id_value or '') | tojson }};
const systemClientSecret = {{ (client_secret_value or '') | tojson }};
// Store integration_id if provided (for per-user OAuth flow)
const integrationId = "{{ integration_id or '' }}";
if (integrationId) {
sessionStorage.setItem('oauth_integration_id', integrationId);
}
// System credentials toggle
const useSystemCredsCheckbox = document.getElementById('use-system-creds');
const customCredsSection = document.getElementById('custom-creds-section');
if (useSystemCredsCheckbox && customCredsSection) {
useSystemCredsCheckbox.addEventListener('change', function() {
if (this.checked) {
customCredsSection.classList.add('hidden');
} else {
customCredsSection.classList.remove('hidden');
}
});
}
// Form elements
const clientIdInput = document.getElementById('client-id');
const clientSecretInput = document.getElementById('client-secret');
const folderIdInput = document.getElementById('folder-id');
const saFolderIdInput = document.getElementById('sa-folder-id');
const startOauthFlowBtn = document.getElementById('start-oauth-flow');
const saveSaSettingsBtn = document.getElementById('save-sa-settings');
const selectFolderBtn = document.getElementById('select-folder-btn');
// Admin-mode elements (may be null in user mode)
const oauthTabBtn = document.getElementById('oauth-tab-btn');
const saTabBtn = document.getElementById('sa-tab-btn');
const oauthTab = document.getElementById('oauth-tab');
const saTab = document.getElementById('sa-tab');
const tokenStatus = document.getElementById('token-status');
const testConnectionBtn = document.getElementById('test-connection');
const refreshTokenBtn = document.getElementById('refresh-token-btn');
const oauthEnvSection = document.getElementById('oauth-env-section');
const saEnvSection = document.getElementById('sa-env-section');
// Modal elements
const resultModal = document.getElementById('resultModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalClose = document.getElementById('modalClose');
// Google Picker variables
let pickerApiLoaded = false;
let pickerOAuthToken = null;
// Google Picker API functions
function loadPickerApi() {
gapi.load('picker', {
'callback': function() {
pickerApiLoaded = true;
}
});
}
// Load the Google API Loader script if select folder button exists
if (selectFolderBtn) {
const script = document.createElement('script');
script.src = 'https://apis.google.com/js/api.js';
script.onload = function() {
loadPickerApi();
};
document.body.appendChild(script);
// Load the GSI Client for OAuth
const gsiScript = document.createElement('script');
gsiScript.src = 'https://accounts.google.com/gsi/client';
document.body.appendChild(gsiScript);
}
// Function to create and show the Google Picker
function createPicker() {
const clientId = clientIdInput.value.trim();
if (!clientId) {
showModal('error', 'Missing Client ID', 'Please enter your Google OAuth Client ID first.');
return;
}
// Check if we already have an active access token
fetch('/api/google-drive/get-token-info')
.then(response => response.json())
.then(data => {
if (data.status === 'success' && data.access_token) {
// If we have a valid access token, use it directly
showPicker(clientId, data.access_token);
} else {
// If no valid token exists, inform the user they need to authenticate first
showModal('error', 'Authentication Required',
'You need to complete OAuth authentication before selecting a folder. ' +
'Please click "Start Authentication Flow" first and then select a folder after authentication.');
}
})
.catch(error => {
console.error('Error checking token:', error);
showModal('error', 'Authentication Required',
'Unable to verify authentication status. Please complete the OAuth flow first.');
});
}
function showPicker(clientId, oauthToken) {
if (!oauthToken) {
showModal('error', 'Authentication Required', 'No access token available. Please authenticate first.');
return;
}
// Use the folders view specifically
const folderView = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setIncludeFolders(true)
.setSelectFolderEnabled(true)
.setMode(google.picker.DocsViewMode.LIST); // Use LIST mode which doesn't require broader permissions
const picker = new google.picker.PickerBuilder()
.addView(folderView)
.setOAuthToken(oauthToken)
.setOrigin(window.location.protocol + '//' + window.location.host)
.setTitle('Select a folder for DocuElevate')
.enableFeature(google.picker.Feature.NAV_HIDDEN) // Hide navigation
.disableFeature(google.picker.Feature.MULTISELECT_ENABLED) // Disable multi-select
.setSelectableMimeTypes('application/vnd.google-apps.folder') // Allow only folder selection
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
function pickerCallback(data) {
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
const folder = data[google.picker.Response.DOCUMENTS][0];
const folderId = folder[google.picker.Document.ID];
const folderName = folder[google.picker.Document.NAME];
// Update the folder ID input
if (folderIdInput) folderIdInput.value = folderId;
if (saFolderIdInput) {
saFolderIdInput.value = folderId;
}
// Automatically save the folder ID to ensure it's stored
saveFolderId(folderId);
// Show success message
showModal('success', 'Folder Selected', `You selected folder: "${folderName}" (ID: ${folderId}) and saved it to your configuration.`);
}
}
// New function to save folder ID after picker selection
function saveFolderId(folderId) {
// Check which tab is active to determine if we're using OAuth or Service Account
const isOauthActive = !oauthTab || !oauthTab.classList.contains('hidden');
// Prepare form data
const formData = new FormData();
formData.append('folder_id', folderId);
formData.append('use_oauth', isOauthActive ? 'true' : 'false');
// If using OAuth, also include client credentials if available
if (isOauthActive && clientIdInput) {
const clientId = clientIdInput.value.trim();
const clientSecret = clientSecretInput ? clientSecretInput.value.trim() : '';
if (clientId) formData.append('client_id', clientId);
if (clientSecret) formData.append('client_secret', clientSecret);
}
// Send the folder ID to be saved server-side
fetch('/api/google-drive/save-settings', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.detail || 'Failed to save folder ID');
});
}
return response.json();
})
.then(data => {
if (data.status !== 'success') {
console.error('Error saving folder ID:', data.message);
} else {
console.log('Folder ID saved successfully');
// Make the token status visible if it was hidden (admin mode)
if (tokenStatus) tokenStatus.classList.remove('hidden');
// Update environment variables display if they exist
updateEnvVarsDisplay();
}
})
.catch(error => {
console.error('Error saving folder ID:', error);
});
}
// Function to update environment variables display
function updateEnvVarsDisplay() {
const folderId = (folderIdInput && folderIdInput.value) || (saFolderIdInput && saFolderIdInput.value) || 'YOUR_FOLDER_ID';
// Update OAuth env vars if the element exists
const oauthEnvVarsCode = document.querySelector('#oauth-env-vars code');
if (oauthEnvVarsCode) {
const clientId = (clientIdInput && clientIdInput.value) || 'YOUR_CLIENT_ID';
const clientSecret = 'YOUR_CLIENT_SECRET';
oauthEnvVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=true
GOOGLE_DRIVE_CLIENT_ID=${clientId}
GOOGLE_DRIVE_CLIENT_SECRET=${clientSecret}
GOOGLE_DRIVE_REFRESH_TOKEN=YOUR_REFRESH_TOKEN
GOOGLE_DRIVE_FOLDER_ID=${folderId}`;
}
// Update Service Account env vars if the element exists
const saEnvVarsCode = document.querySelector('#sa-env-vars code');
if (saEnvVarsCode) {
saEnvVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=false
GOOGLE_DRIVE_FOLDER_ID=${folderId}
# Make sure to add your GOOGLE_DRIVE_CREDENTIALS_JSON to your .env file`;
}
}
// Modal functions
function showModal(status, title, message) {
modalTitle.textContent = title;
modalMessage.textContent = message;
// Set the appropriate icon
if (status === 'success') {
modalIcon.innerHTML = `
<svg class="h-6 w-6 text-green-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
`;
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4';
} else {
modalIcon.innerHTML = `
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
`;
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4';
}
resultModal.classList.remove('hidden');
}
function hideModal() {
resultModal.classList.add('hidden');
}
// Close modal when clicking the close button or outside
modalClose.addEventListener('click', hideModal);
resultModal.addEventListener('click', function(e) {
if (e.target === resultModal) {
hideModal();
}
});
// Attach Google Picker button event listener
if (selectFolderBtn) {
selectFolderBtn.addEventListener('click', createPicker);
}
// Tab switching (admin mode only)
if (oauthTabBtn && saTabBtn && oauthTab && saTab) {
oauthTabBtn.addEventListener('click', function() {
oauthTabBtn.className = 'px-4 py-2 rounded-md text-white bg-blue-600';
saTabBtn.className = 'px-4 py-2 rounded-md text-gray-700 bg-gray-200';
oauthTab.classList.remove('hidden');
saTab.classList.add('hidden');
if (oauthEnvSection) oauthEnvSection.classList.remove('hidden');
if (saEnvSection) saEnvSection.classList.add('hidden');
});
saTabBtn.addEventListener('click', function() {
saTabBtn.className = 'px-4 py-2 rounded-md text-white bg-blue-600';
oauthTabBtn.className = 'px-4 py-2 rounded-md text-gray-700 bg-gray-200';
saTab.classList.remove('hidden');
oauthTab.classList.add('hidden');
if (saEnvSection) saEnvSection.classList.remove('hidden');
if (oauthEnvSection) oauthEnvSection.classList.add('hidden');
});
}
// Sync folder IDs between tabs (admin mode only)
if (folderIdInput && saFolderIdInput) {
folderIdInput.addEventListener('input', function() {
saFolderIdInput.value = folderIdInput.value;
});
saFolderIdInput.addEventListener('input', function() {
folderIdInput.value = saFolderIdInput.value;
});
}
// Start OAuth flow button
if (startOauthFlowBtn) {
startOauthFlowBtn.addEventListener('click', function() {
// Determine which credentials to use
const useSystemCreds = hasSystemCredentials && useSystemCredsCheckbox && useSystemCredsCheckbox.checked;
const clientId = useSystemCreds ? systemClientId : clientIdInput.value.trim();
const clientSecret = useSystemCreds ? systemClientSecret : clientSecretInput.value.trim();
const folderId = folderIdInput ? folderIdInput.value.trim() : '';
if (!clientId) {
showModal('error', 'Validation Error', 'Please enter your Client ID');
return;
}
if (!clientSecret) {
showModal('error', 'Validation Error', 'Please enter your Client Secret');
return;
}
// Save values to session storage for use after redirect
sessionStorage.setItem('google_drive_client_id', clientId);
sessionStorage.setItem('google_drive_client_secret', clientSecret);
if (useSystemCreds) {
sessionStorage.setItem('google_drive_use_system_creds', 'true');
}
// In admin mode store folder_id; in user mode the config is already set
if (!userMode && folderId) {
sessionStorage.setItem('google_drive_folder_id', folderId);
}
sessionStorage.setItem('google_drive_use_oauth', 'true');
// Create redirect URI
const redirectUri = `${window.location.origin}/google-drive-callback`;
// Redirect to auth start endpoint
window.location.href = `/google-drive-auth-start?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
});
}
// Save service account settings button (admin mode only)
if (saveSaSettingsBtn) {
saveSaSettingsBtn.addEventListener('click', function() {
const folderId = saFolderIdInput.value.trim();
if (!folderId) {
showModal('error', 'Validation Error', 'Please enter your Google Drive Folder ID');
return;
}
// Prepare form data
const formData = new FormData();
formData.append('folder_id', folderId);
formData.append('use_oauth', 'false');
// Send update request
const originalText = saveSaSettingsBtn.textContent;
saveSaSettingsBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Saving...';
saveSaSettingsBtn.disabled = true;
fetch('/api/google-drive/save-settings', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.detail || 'Failed to save settings');
});
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
showModal('success', 'Settings Saved', 'Google Drive service account settings have been saved');
if (tokenStatus) tokenStatus.classList.remove('hidden');
// Update environment variables display
const saEnvVarsCode = document.querySelector('#sa-env-vars code');
if (saEnvVarsCode) {
saEnvVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=false
GOOGLE_DRIVE_FOLDER_ID=${folderId}
# Make sure to add your GOOGLE_DRIVE_CREDENTIALS_JSON to your .env file`;
}
} else {
showModal('error', 'Error', data.message || 'Failed to save settings');
}
})
.catch(error => {
showModal('error', 'Error', error.message);
})
.finally(() => {
saveSaSettingsBtn.textContent = originalText;
saveSaSettingsBtn.disabled = false;
});
});
}
// Test connection button (admin mode only)
if (testConnectionBtn) {
testConnectionBtn.addEventListener('click', function() {
const originalText = testConnectionBtn.textContent;
testConnectionBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
testConnectionBtn.disabled = true;
fetch('/api/google-drive/test-token')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showModal('success', 'Connection Test Successful', data.message);
} else {
if (data.needs_reauth) {
showModal('error', 'Authentication Required', 'Your token has expired or is invalid. Please reconfigure the connection.');
} else {
showModal('error', 'Connection Test Failed', data.message);
}
}
})
.catch(error => {
showModal('error', 'Connection Error', 'Error testing connection: ' + error.message);
})
.finally(() => {
testConnectionBtn.textContent = originalText;
testConnectionBtn.disabled = false;
});
});
}
// Refresh token button (admin mode only)
if (refreshTokenBtn) {
refreshTokenBtn.addEventListener('click', function() {
showModal('info', 'Confirm', 'This will start a new OAuth flow to obtain a fresh token. Continue?');
modalClose.textContent = "Cancel";
// Add a confirm button
const confirmBtn = document.createElement('button');
confirmBtn.className = 'ml-2 px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300';
confirmBtn.textContent = 'Continue';
confirmBtn.addEventListener('click', function() {
hideModal();
// Check if we have necessary info before starting flow
if (!clientIdInput.value.trim() || !clientSecretInput.value.trim()) {
showModal('error', 'Missing Information', 'Please enter your Client ID and Client Secret first');
return;
}
startOauthFlowBtn.click();
});
// Add to modal
modalClose.parentNode.appendChild(confirmBtn);
// Clean up modal when closed
const onModalClose = function() {
if (confirmBtn.parentNode) {
confirmBtn.parentNode.removeChild(confirmBtn);
}
modalClose.textContent = "Close";
resultModal.removeEventListener('hidden', onModalClose);
};
resultModal.addEventListener('hidden', onModalClose);
});
}
// Copy environment variables buttons (admin mode only)
const copyOAuthEnvVarsBtn = document.getElementById('copy-oauth-env-vars');
const copySAEnvVarsBtn = document.getElementById('copy-sa-env-vars');
if (copyOAuthEnvVarsBtn) {
copyOAuthEnvVarsBtn.addEventListener('click', function() {
const envVarsText = document.getElementById('oauth-env-vars').textContent;
navigator.clipboard.writeText(envVarsText)
.then(() => {
copyOAuthEnvVarsBtn.textContent = 'Copied!';
copyOAuthEnvVarsBtn.classList.add('bg-green-700');
setTimeout(() => {
copyOAuthEnvVarsBtn.textContent = 'Copy';
copyOAuthEnvVarsBtn.classList.remove('bg-green-700');
}, 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
});
}
if (copySAEnvVarsBtn) {
copySAEnvVarsBtn.addEventListener('click', function() {
const envVarsText = document.getElementById('sa-env-vars').textContent;
navigator.clipboard.writeText(envVarsText)
.then(() => {
copySAEnvVarsBtn.textContent = 'Copied!';
copySAEnvVarsBtn.classList.add('bg-green-700');
setTimeout(() => {
copySAEnvVarsBtn.textContent = 'Copy';
copySAEnvVarsBtn.classList.remove('bg-green-700');
}, 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
});
}
});
</script>
{% endblock %}