feat(integrations): add per-user OAuth wizard with user-mode for Dropbox, OneDrive, Google Drive

- Add user_mode to dropbox/onedrive/google_drive setup views that loads integration config
- Show user-friendly auth wizard when integration_id is provided (user mode)
- In user mode: show integration name, current folder, back-to-integrations link
- In callback templates: only save credentials (not config) for user integrations
- In integrations dashboard: show Authorize/Re-Authorize button for all OAuth types
- Add WATCH_FOLDER OAuth support: detect source_type in config for auth button
- isOAuthType() and oauthLink() now accept full integration object

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-09 18:24:07 +00:00
parent 80b1b01beb
commit c71f33a214
10 changed files with 473 additions and 131 deletions
+122 -52
View File
@@ -4,6 +4,37 @@
{% 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.
@@ -42,8 +73,10 @@
</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>
@@ -224,7 +257,39 @@
</div>
</div>
<!-- Token validation and status -->
</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>
@@ -262,7 +327,7 @@
</button>
</div>
<!-- Configuration for Worker Nodes section -->
<!-- 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">
@@ -307,6 +372,7 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
</p>
</div>
</div>
{% endif %}
<div class="bg-white shadow-md rounded-lg p-6 mt-8 mb-8">
<div class="flex items-start">
@@ -326,9 +392,15 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
</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 -->
@@ -358,18 +430,14 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const userMode = {{ 'true' if user_mode else 'false' }};
// Store integration_id if provided (for per-user OAuth flow)
const integrationId = "{{ integration_id or '' }}";
if (integrationId) {
sessionStorage.setItem('oauth_integration_id', integrationId);
}
// 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');
@@ -379,12 +447,14 @@ document.addEventListener('DOMContentLoaded', function() {
const saveSaSettingsBtn = document.getElementById('save-sa-settings');
const selectFolderBtn = document.getElementById('select-folder-btn');
// Status and test elements
// 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');
// Environment sections
const oauthEnvSection = document.getElementById('oauth-env-section');
const saEnvSection = document.getElementById('sa-env-section');
@@ -485,7 +555,7 @@ document.addEventListener('DOMContentLoaded', function() {
const folderName = folder[google.picker.Document.NAME];
// Update the folder ID input
folderIdInput.value = folderId;
if (folderIdInput) folderIdInput.value = folderId;
if (saFolderIdInput) {
saFolderIdInput.value = folderId;
}
@@ -501,7 +571,7 @@ document.addEventListener('DOMContentLoaded', function() {
// 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');
const isOauthActive = !oauthTab || !oauthTab.classList.contains('hidden');
// Prepare form data
const formData = new FormData();
@@ -509,9 +579,9 @@ document.addEventListener('DOMContentLoaded', function() {
formData.append('use_oauth', isOauthActive ? 'true' : 'false');
// If using OAuth, also include client credentials if available
if (isOauthActive) {
if (isOauthActive && clientIdInput) {
const clientId = clientIdInput.value.trim();
const clientSecret = clientSecretInput.value.trim();
const clientSecret = clientSecretInput ? clientSecretInput.value.trim() : '';
if (clientId) formData.append('client_id', clientId);
if (clientSecret) formData.append('client_secret', clientSecret);
@@ -535,8 +605,8 @@ document.addEventListener('DOMContentLoaded', function() {
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');
// 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();
@@ -549,13 +619,13 @@ document.addEventListener('DOMContentLoaded', function() {
// Function to update environment variables display
function updateEnvVarsDisplay() {
const folderId = folderIdInput.value || saFolderIdInput.value || 'YOUR_FOLDER_ID';
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.value || 'YOUR_CLIENT_ID';
const clientSecret = clientSecretInput.value ? 'YOUR_CLIENT_SECRET' : 'YOUR_CLIENT_SECRET';
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}
@@ -615,33 +685,33 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
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');
});
// 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');
saEnvSection.classList.remove('hidden');
oauthEnvSection.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
folderIdInput.addEventListener('input', function() {
if (saFolderIdInput) {
// Sync folder IDs between tabs (admin mode only)
if (folderIdInput && saFolderIdInput) {
folderIdInput.addEventListener('input', function() {
saFolderIdInput.value = folderIdInput.value;
}
});
});
if (saFolderIdInput) {
saFolderIdInput.addEventListener('input', function() {
folderIdInput.value = saFolderIdInput.value;
});
@@ -652,7 +722,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
startOauthFlowBtn.addEventListener('click', function() {
const clientId = clientIdInput.value.trim();
const clientSecret = clientSecretInput.value.trim();
const folderId = folderIdInput.value.trim();
const folderId = folderIdInput ? folderIdInput.value.trim() : '';
if (!clientId) {
showModal('error', 'Validation Error', 'Please enter your Client ID');
@@ -664,12 +734,12 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
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) {
// 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');
@@ -682,7 +752,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
});
}
// Save service account settings button
// Save service account settings button (admin mode only)
if (saveSaSettingsBtn) {
saveSaSettingsBtn.addEventListener('click', function() {
const folderId = saFolderIdInput.value.trim();
@@ -717,7 +787,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
.then(data => {
if (data.status === 'success') {
showModal('success', 'Settings Saved', 'Google Drive service account settings have been saved');
tokenStatus.classList.remove('hidden');
if (tokenStatus) tokenStatus.classList.remove('hidden');
// Update environment variables display
const saEnvVarsCode = document.querySelector('#sa-env-vars code');
@@ -740,7 +810,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
});
}
// Test connection button
// Test connection button (admin mode only)
if (testConnectionBtn) {
testConnectionBtn.addEventListener('click', function() {
const originalText = testConnectionBtn.textContent;
@@ -770,7 +840,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
});
}
// Refresh token button
// 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?');
@@ -808,7 +878,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
});
}
// Copy environment variables buttons
// Copy environment variables buttons (admin mode only)
const copyOAuthEnvVarsBtn = document.getElementById('copy-oauth-env-vars');
const copySAEnvVarsBtn = document.getElementById('copy-sa-env-vars');