Add Google Drive authorization processing and error handling templates

- Implemented `google_drive_callback.html` for processing Google Drive authorization, including UI for success and error states.
- Added JavaScript functionality for exchanging authorization codes, saving settings, and handling folder selection.
- Created `google_drive_callback_error.html` to display error messages during the authorization process.
This commit is contained in:
Christian Krakau-Louis
2025-04-11 00:13:57 +02:00
parent b258dd2f29
commit 4c50c0aae6
26 changed files with 3297 additions and 727 deletions
+6
View File
@@ -185,12 +185,18 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('error-container').classList.remove('hidden');
document.getElementById('error-message').innerText = message;
// Hide the spinner when showing error
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
}
function showSuccess(refreshToken, appKey, appSecret, folderPath) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('success-container').classList.remove('hidden');
// Hide the spinner when showing success
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
// Update the environment variables pre block with the new token
const envVarsCode = document.querySelector('#env-vars code');
if (envVarsCode) {
+846
View File
@@ -0,0 +1,846 @@
{% extends "base.html" %}
{% block title %}Google Drive Setup{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
<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>
</div>
<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">
<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 }}">
</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 }}">
</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>
<!-- Token validation and status -->
<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 -->
<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>
<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">
<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>
</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">
<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() {
// Tab elements
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');
// 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');
// Status and test elements
const tokenStatus = document.getElementById('token-status');
const testConnectionBtn = document.getElementById('test-connection');
const refreshTokenBtn = document.getElementById('refresh-token-btn');
// Environment sections
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
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.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) {
const clientId = clientIdInput.value.trim();
const clientSecret = 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
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.value || 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.value || 'YOUR_CLIENT_ID';
const clientSecret = clientSecretInput.value ? 'YOUR_CLIENT_SECRET' : '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
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');
oauthEnvSection.classList.remove('hidden');
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');
saEnvSection.classList.remove('hidden');
oauthEnvSection.classList.add('hidden');
});
// Sync folder IDs between tabs
folderIdInput.addEventListener('input', function() {
if (saFolderIdInput) {
saFolderIdInput.value = folderIdInput.value;
}
});
if (saFolderIdInput) {
saFolderIdInput.addEventListener('input', function() {
folderIdInput.value = saFolderIdInput.value;
});
}
// Start OAuth flow button
if (startOauthFlowBtn) {
startOauthFlowBtn.addEventListener('click', function() {
const clientId = clientIdInput.value.trim();
const clientSecret = clientSecretInput.value.trim();
const folderId = 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;
}
// Don't require folder ID, make it optional
// Save values to session storage for use after redirect
sessionStorage.setItem('google_drive_client_id', clientId);
sessionStorage.setItem('google_drive_client_secret', clientSecret);
if (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
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');
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
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
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
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 %}
@@ -0,0 +1,476 @@
{% extends "base.html" %}
{% block title %}Google Drive Authorization Processing{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="bg-white shadow-md rounded-lg p-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">
<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>
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
<p class="text-gray-600 mt-2">Please wait while we complete the Google Drive authorization process...</p>
</div>
<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>
<div id="processing-message" class="text-center text-gray-700">
<p>Exchanging authorization code for refresh token...</p>
</div>
<div id="error-container" class="hidden mt-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
<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">
<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>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Error details:</h3>
<div class="mt-2 text-sm text-red-700" id="error-message">
<!-- Error message will be inserted here -->
</div>
</div>
</div>
</div>
<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">
Return to Setup
</a>
</div>
</div>
<div id="folder-selection-container" class="hidden mt-6">
<div class="rounded-md bg-blue-50 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 font-medium text-blue-800">
Authentication successful! Please select or enter the Google Drive folder where you want to save files.
</p>
</div>
</div>
</div>
<div class="space-y-4 mb-6">
<div>
<label for="folder-id-input" class="block text-sm font-medium text-gray-700">Google Drive Folder ID</label>
<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">
<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">
<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>
Select Folder
</button>
</div>
<p class="mt-2 text-sm text-gray-500">
You can paste a folder ID directly or use the selector to pick a folder. Root folder is "root".
</p>
</div>
<div>
<button id="save-folder-btn" 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 Settings
</button>
</div>
</div>
</div>
<div id="success-container" class="hidden mt-6">
<div class="rounded-md bg-green-50 p-4">
<div class="flex">
<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">
<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>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-green-800">
Google Drive authorization successful! Your refresh token has been saved.
</p>
</div>
</div>
</div>
<div class="mt-6 p-4 bg-gray-100 rounded-md">
<h3 class="font-medium text-lg mb-2">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="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code></code></pre>
<button id="copy-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 class="mt-4 text-center">
<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">
Go to Status Page
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const code = "{{ code }}";
// Get credentials from session storage
const clientId = sessionStorage.getItem('google_drive_client_id');
const clientSecret = sessionStorage.getItem('google_drive_client_secret');
const folderId = sessionStorage.getItem('google_drive_folder_id');
const redirectUri = window.location.origin + "/google-drive-callback";
let accessToken = null;
let refreshToken = null;
// Define folderIdInput at the top level so it's accessible throughout the script
const folderIdInput = document.getElementById('folder-id-input');
const folderSelectBtn = document.getElementById('folder-select-picker-btn');
const saveFolderBtn = document.getElementById('save-folder-btn');
// Automatically exchange the code for a refresh token
if (code) {
if (!clientId || !clientSecret) {
showError("Missing Client ID or Client Secret. Please go back to the setup page and try again.");
return;
}
exchangeCode(code, clientId, clientSecret, redirectUri, folderId);
} else {
showError("No authorization code was found in the URL");
}
function exchangeCode(code, clientId, clientSecret, redirectUri, folderId) {
const formData = new FormData();
formData.append('client_id', clientId);
formData.append('client_secret', clientSecret);
formData.append('redirect_uri', redirectUri);
formData.append('code', code);
if (folderId) {
formData.append('folder_id', folderId);
}
document.getElementById('processing-message').innerHTML =
'<p>Exchanging authorization code for refresh token...</p>';
fetch('/api/google-drive/exchange-token', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.detail || 'Failed to exchange token');
});
}
return response.json();
})
.then(data => {
if (data.refresh_token) {
// Store tokens
refreshToken = data.refresh_token;
accessToken = data.access_token;
// Update settings in memory first
const updateFormData = new FormData();
updateFormData.append('refresh_token', data.refresh_token);
// Use the client ID and client secret from session storage
updateFormData.append('client_id', clientId);
updateFormData.append('client_secret', clientSecret);
if (folderId) {
updateFormData.append('folder_id', folderId);
}
updateFormData.append('use_oauth', 'true');
document.getElementById('processing-message').innerHTML =
'<p>Updating system settings with new token...</p>';
return fetch('/api/google-drive/update-settings', {
method: 'POST',
body: updateFormData
}).then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error('Token received but failed to update settings: ' + (err.detail || 'Unknown error'));
});
}
return response.json();
}).then(() => {
// If we have a folder ID, save settings directly
if (folderId) {
return saveSettings(data.refresh_token, clientId, clientSecret, folderId);
} else {
// Show folder selection UI
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('folder-selection-container').classList.remove('hidden');
// Initialize Google Picker for folder selection
loadGooglePicker(accessToken, clientId);
return null; // Return null to avoid further .then() processing
}
});
} else {
throw new Error('No refresh token was received from the server');
}
})
.then(result => {
// Only proceed if we saved settings directly
if (result) {
showSuccess(result.refresh_token, result.client_id, result.client_secret, result.folderId);
}
})
.catch(error => {
showError(error.message);
});
}
function showError(message) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('folder-selection-container').classList.add('hidden');
document.getElementById('error-container').classList.remove('hidden');
document.getElementById('error-message').innerText = message;
// Hide the spinner when showing error
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
}
function saveSettings(refreshToken, clientId, clientSecret, folderId) {
const saveFormData = new FormData();
saveFormData.append('refresh_token', refreshToken);
saveFormData.append('client_id', clientId);
saveFormData.append('client_secret', clientSecret);
if (folderId) {
saveFormData.append('folder_id', folderId);
}
saveFormData.append('use_oauth', 'true');
document.getElementById('processing-message').innerHTML =
'<p>Saving settings to configuration...</p>';
return fetch('/api/google-drive/save-settings', {
method: 'POST',
body: saveFormData
}).then(response => {
if (!response.ok) {
return response.json().then(err => {
// Check if the error is about .env file not found
if (err.detail && err.detail.includes('.env file not found')) {
// Continue despite .env file issue
return {
status: 'warning',
message: 'Settings saved in memory only. The .env file could not be updated.',
in_memory_only: true
};
}
throw new Error('Failed to save settings to file: ' + (err.detail || 'Unknown error'));
});
}
return response.json();
}).then((result) => {
// Return the values for showing success screen
return {
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
folderId: folderId,
in_memory_only: result.in_memory_only || false
};
});
}
function showSuccess(refreshToken, clientId, clientSecret, folderId, inMemoryOnly) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('folder-selection-container').classList.add('hidden');
document.getElementById('success-container').classList.remove('hidden');
// Hide the spinner when showing success
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
// Update the environment variables pre block with the new token
const envVarsCode = document.querySelector('#env-vars code');
if (envVarsCode) {
envVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=true
GOOGLE_DRIVE_CLIENT_ID=${clientId}
GOOGLE_DRIVE_CLIENT_SECRET=${clientSecret}
GOOGLE_DRIVE_REFRESH_TOKEN=${refreshToken}
GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
}
// If settings were only saved in memory, add a warning
if (inMemoryOnly) {
const successContainer = document.getElementById('success-container');
const warningDiv = document.createElement('div');
warningDiv.className = 'rounded-md bg-yellow-50 p-4 mt-4';
warningDiv.innerHTML = `
<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 font-medium text-yellow-800">
Note: Settings were saved in memory only. The .env file could not be updated.
Make sure to add these environment variables to your configuration files manually.
</p>
</div>
</div>
`;
// Insert warning after the success message but before the environment vars section
const envVarsSection = document.querySelector('#success-container .mt-6');
successContainer.insertBefore(warningDiv, envVarsSection);
}
// Add copy functionality
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
copyEnvVarsBtn.addEventListener('click', function() {
const envVarsText = document.getElementById('env-vars').textContent;
navigator.clipboard.writeText(envVarsText)
.then(() => {
const originalText = copyEnvVarsBtn.textContent;
copyEnvVarsBtn.textContent = 'Copied!';
copyEnvVarsBtn.classList.add('bg-green-700');
setTimeout(() => {
copyEnvVarsBtn.textContent = originalText;
copyEnvVarsBtn.classList.remove('bg-green-700');
}, 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
alert('Failed to copy text to clipboard');
});
});
}
// Clear session storage
sessionStorage.removeItem('google_drive_client_id');
sessionStorage.removeItem('google_drive_client_secret');
sessionStorage.removeItem('google_drive_folder_id');
// In 10 seconds, redirect to status page
setTimeout(() => {
window.location.href = '/status';
}, 10000);
}
// Handle folder selection UI
if (saveFolderBtn) {
saveFolderBtn.addEventListener('click', function() {
if (!folderIdInput) {
alert('Error: Folder input element not found');
return;
}
const folderId = folderIdInput.value.trim() || 'root';
saveFolderBtn.disabled = true;
saveFolderBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Saving...';
saveSettings(refreshToken, clientId, clientSecret, folderId)
.then(result => {
showSuccess(result.refresh_token, result.client_id, result.client_secret, result.folderId);
})
.catch(error => {
alert('Failed to save settings: ' + error.message);
saveFolderBtn.disabled = false;
saveFolderBtn.textContent = 'Save Settings';
});
});
}
// Function to load and initialize the Google Picker
function loadGooglePicker(accessToken, clientId) {
// Load the Google API Loader script
const script = document.createElement('script');
script.src = 'https://apis.google.com/js/api.js';
script.onload = function() {
gapi.load('picker', function() {
initGooglePicker(accessToken, clientId);
});
};
document.body.appendChild(script);
}
// Initialize and setup the Google Picker
function initGooglePicker(accessToken, clientId) {
if (!accessToken || !clientId || !folderSelectBtn) {
return;
}
// Setup the click handler for the folder select button
folderSelectBtn.addEventListener('click', function() {
// Create the folder picker view
const folderView = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setIncludeFolders(true)
.setSelectFolderEnabled(true)
.setMode(google.picker.DocsViewMode.LIST); // Use LIST mode to work with the drive.file scope
// Create and render the picker
const picker = new google.picker.PickerBuilder()
.addView(folderView)
.setOAuthToken(accessToken)
.setOrigin(window.location.protocol + '//' + window.location.host)
.setTitle('Select a folder for DocuElevate')
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
});
}
// Callback function for picker
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;
// Add visual confirmation instead of alert
const confirmationMsg = document.createElement('div');
confirmationMsg.className = 'mt-2 text-sm text-green-600';
confirmationMsg.innerHTML = `<svg class="inline-block h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<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> Selected folder: "${folderName}"`;
// Remove previous confirmation if it exists
const existingConfirmation = folderIdInput.parentNode.querySelector('.text-green-600');
if (existingConfirmation) {
existingConfirmation.remove();
}
// Insert the confirmation message after the input field
folderIdInput.parentNode.appendChild(confirmationMsg);
}
}
}
});
</script>
{% endblock %}
@@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}Google Drive Authorization Error{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="bg-white shadow-md rounded-lg p-6">
<div class="text-center mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 class="text-2xl font-bold mt-4">Authorization Failed</h2>
<p class="text-gray-600 mt-2">Sorry, we couldn't complete the Google Drive authorization.</p>
</div>
<div class="mb-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
<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" 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" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Error details:</h3>
<div class="mt-2 text-sm text-red-700">
<p>{{ error }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6">
<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">
Return to Setup
</a>
</div>
</div>
</div>
{% endblock %}
@@ -194,12 +194,18 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('error-container').classList.remove('hidden');
document.getElementById('error-message').innerText = message;
// Hide the spinner when showing error
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
}
function showSuccess(refreshToken, clientId, clientSecret, tenantId, folderPath) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('success-container').classList.remove('hidden');
// Hide the spinner when showing success
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
// Update the environment variables pre block with the new token
const envVarsCode = document.querySelector('#env-vars code');
if (envVarsCode) {
+3
View File
@@ -58,6 +58,9 @@
<p class="text-gray-700">
<strong>No Additional Sharing:</strong> We do not share, sell, or otherwise disclose your personal data to third parties for advertising or marketing purposes.
</p>
<p class="text-gray-700 mt-3">
<strong>Cloud Storage Integration:</strong> We integrate with Google Drive, Dropbox, OneDrive and other storage providers to enable you to save and access your documents across services.
</p>
</div>
<div class="bg-white shadow rounded-lg p-6 mb-6">
+38 -2
View File
@@ -136,6 +136,22 @@
Configure Now
</a>
{% endif %}
{% elif name == "Google Drive" %}
{% if provider.configured %}
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="google_drive">
Test Connection
</button>
<a href="/google-drive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-gear h-3 w-3 mr-1"></i>
Manage
</a>
{% else %}
<a href="/google-drive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Configure Now
</a>
{% endif %}
{% elif name == "OpenAI" %}
{% if provider.configured %}
<button
@@ -396,6 +412,8 @@ document.addEventListener('DOMContentLoaded', function() {
endpoint = '/api/dropbox/test-token';
} else if (provider === 'onedrive') {
endpoint = '/api/onedrive/test-token';
} else if (provider === 'google_drive') {
endpoint = '/api/google-drive/test-token';
} else if (provider === 'openai') {
endpoint = '/api/openai/test';
} else if (provider === 'azure') {
@@ -406,14 +424,32 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showModal('success', 'Connection Test Successful', `${data.message} ${data.account ? 'as ' + data.account : ''}`);
// Create successful message
let message = data.message || 'Connection successful';
// Add token expiration info if available (especially for Google Drive)
if (data.token_info && data.token_info.expires_in_human) {
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
<span class="font-medium">Token valid for:</span> ${data.token_info.expires_in_human}
</div>`;
// Show the message with HTML
modalTitle.textContent = 'Connection Test Successful';
modalMessage.innerHTML = message;
modalIcon.innerHTML = '<i class="fa-solid fa-check text-green-600 fa-2x"></i>';
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4';
resultModal.classList.remove('hidden');
} else {
// Use the regular modal without HTML
showModal('success', 'Connection Test Successful', `${data.message} ${data.account ? 'as ' + data.account : ''}`);
}
} else {
if (data.needs_reauth) {
showModal('error', 'Authentication Required', 'Your token has expired or is invalid. Please reconfigure this connection.');
// Add option to go to config page
modalClose.textContent = "Configure Now";
modalClose.addEventListener('click', function redirectToConfig() {
window.location.href = `/${provider}-setup`;
window.location.href = `/${provider.replace('_', '-')}-setup`;
modalClose.removeEventListener('click', redirectToConfig);
}, { once: true });
} else {