b3a238744d
Added escapeHtml() utility function to both Dropbox and OneDrive callback pages. Folder names, paths, and error messages inserted into innerHTML via template literals are now escaped to prevent potential cross-site scripting from malicious folder names. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
444 lines
20 KiB
HTML
444 lines
20 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Dropbox 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" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</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 Dropbox 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" role="status" aria-label="Loading"></div>
|
|
</div>
|
|
|
|
<div id="processing-message" class="text-center text-gray-700" aria-live="polite">
|
|
<p>Exchanging authorization code for refresh token...</p>
|
|
</div>
|
|
|
|
<div id="error-container" class="hidden mt-6" role="alert" aria-live="assertive">
|
|
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
|
<div class="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" id="error-message">
|
|
<!-- Error message will be inserted here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4">
|
|
<a id="return-to-setup-link" href="/dropbox-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
Return to Setup
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="success-container" class="hidden mt-6" aria-live="polite">
|
|
<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" 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>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-green-800">
|
|
Dropbox authorization successful! Your refresh token has been saved.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Folder selector (shown after authorization) -->
|
|
<div id="folder-selector" class="hidden mt-6 p-4 bg-white border border-gray-200 rounded-lg">
|
|
<h3 class="font-medium text-lg mb-3">
|
|
<svg class="inline-block h-5 w-5 mr-1 text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" />
|
|
<path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z" />
|
|
</svg>
|
|
Select Folder
|
|
</h3>
|
|
<p class="text-sm text-gray-600 mb-3">Browse your Dropbox to select a folder for this integration.</p>
|
|
|
|
<div class="mb-3">
|
|
<div id="folder-breadcrumb" class="flex items-center text-sm text-gray-500 mb-2">
|
|
<button type="button" class="folder-nav-btn text-indigo-600 hover:text-indigo-800 font-medium" data-path="" aria-label="Navigate to root folder">/ Root</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="folder-list" class="border border-gray-200 rounded-md max-h-64 overflow-y-auto">
|
|
<div class="p-4 text-center text-gray-500">
|
|
<div class="animate-spin inline-block h-5 w-5 border-2 border-gray-300 border-t-indigo-600 rounded-full mb-2" role="status" aria-label="Loading folders"></div>
|
|
<p class="text-sm">Loading folders…</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 flex items-center gap-3">
|
|
<label for="selected-folder-path" class="text-sm font-medium text-gray-700 whitespace-nowrap">Selected folder:</label>
|
|
<input id="selected-folder-path" type="text" class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm" placeholder="/" />
|
|
<button id="save-folder-btn" type="button" 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">
|
|
Save Folder
|
|
</button>
|
|
</div>
|
|
<p id="folder-save-status" class="mt-2 text-sm hidden" aria-live="polite"></p>
|
|
</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 (these take precedence over server-provided values)
|
|
const appKey = sessionStorage.getItem('dropbox_app_key') || "{{ app_key_value }}";
|
|
const appSecret = sessionStorage.getItem('dropbox_app_secret') || "{{ app_secret_value }}";
|
|
const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads';
|
|
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
|
|
|
// In user mode, point the "Return to Setup" link back to the wizard with integration_id
|
|
if (integrationId) {
|
|
const returnLink = document.getElementById('return-to-setup-link');
|
|
if (returnLink) {
|
|
returnLink.href = `/dropbox-setup?integration_id=${encodeURIComponent(integrationId)}`;
|
|
}
|
|
}
|
|
|
|
const redirectUri = window.location.origin + "/dropbox-callback";
|
|
|
|
// Automatically exchange the code for a refresh token
|
|
if (code) {
|
|
if (!appKey || !appSecret) {
|
|
showError("Missing App Key or App Secret. Please go back to the setup page and try again.");
|
|
return;
|
|
}
|
|
|
|
exchangeCode(code, appKey, appSecret, redirectUri);
|
|
} else {
|
|
showError("No authorization code was found in the URL");
|
|
}
|
|
|
|
function exchangeCode(code, appKey, appSecret, redirectUri) {
|
|
const formData = new FormData();
|
|
formData.append('client_id', appKey);
|
|
formData.append('client_secret', appSecret);
|
|
formData.append('redirect_uri', redirectUri);
|
|
formData.append('code', code);
|
|
formData.append('folder_path', folderPath);
|
|
|
|
document.getElementById('processing-message').innerHTML =
|
|
'<p>Exchanging authorization code for refresh token...</p>';
|
|
|
|
fetch('/api/dropbox/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) {
|
|
// Per-user flow: save to the user's integration record
|
|
if (integrationId) {
|
|
const creds = {
|
|
refresh_token: data.refresh_token,
|
|
app_key: appKey,
|
|
app_secret: appSecret,
|
|
};
|
|
|
|
// Only send credentials — the integration's config (folder, source_type, etc.)
|
|
// is already set and must not be overwritten here.
|
|
const body = { credentials: creds };
|
|
|
|
document.getElementById('processing-message').innerHTML =
|
|
'<p>Saving credentials to your integration...</p>';
|
|
|
|
return fetch(`/api/integrations/${integrationId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
}).then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(err => {
|
|
throw new Error('Failed to save credentials: ' + (err.detail || 'Unknown error'));
|
|
});
|
|
}
|
|
return response.json();
|
|
}).then(() => {
|
|
// Clean up session storage
|
|
sessionStorage.removeItem('dropbox_app_key');
|
|
sessionStorage.removeItem('dropbox_app_secret');
|
|
sessionStorage.removeItem('dropbox_folder_path');
|
|
sessionStorage.removeItem('oauth_integration_id');
|
|
sessionStorage.removeItem('dropbox_use_system_creds');
|
|
|
|
// Hide processing spinner, show success
|
|
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
|
document.getElementById('processing-message').innerHTML =
|
|
'<p class="text-green-600 font-semibold">✓ Dropbox authorized successfully!</p>';
|
|
document.getElementById('success-container').classList.remove('hidden');
|
|
|
|
// Show folder browser with the access token
|
|
initFolderBrowser(data.access_token, integrationId);
|
|
});
|
|
}
|
|
|
|
// Global flow: update settings in memory
|
|
const updateFormData = new FormData();
|
|
updateFormData.append('refresh_token', data.refresh_token);
|
|
|
|
// Use the app key and app secret from session storage
|
|
if (appKey) updateFormData.append('app_key', appKey);
|
|
if (appSecret) updateFormData.append('app_secret', appSecret);
|
|
if (folderPath) updateFormData.append('folder_path', folderPath);
|
|
|
|
document.getElementById('processing-message').innerHTML =
|
|
'<p>Updating system settings with new token...</p>';
|
|
|
|
return fetch('/api/dropbox/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(() => {
|
|
// Show the success message and environment variables
|
|
showSuccess(data.refresh_token, appKey, appSecret, folderPath);
|
|
// In 10 seconds, redirect to status page (giving time to copy)
|
|
setTimeout(() => {
|
|
window.location.href = '/status';
|
|
}, 10000);
|
|
|
|
// Clean up session storage
|
|
sessionStorage.removeItem('dropbox_app_key');
|
|
sessionStorage.removeItem('dropbox_app_secret');
|
|
sessionStorage.removeItem('dropbox_folder_path');
|
|
});
|
|
} else {
|
|
throw new Error('No refresh token was received from the server');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showError(error.message);
|
|
});
|
|
}
|
|
|
|
function showError(message) {
|
|
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) {
|
|
envVarsCode.textContent = `DROPBOX_APP_KEY=${appKey}
|
|
DROPBOX_APP_SECRET=${appSecret}
|
|
DROPBOX_REFRESH_TOKEN=${refreshToken}
|
|
DROPBOX_FOLDER=${folderPath || '/Documents/Uploads'}`;
|
|
}
|
|
|
|
// 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');
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Folder browser ────────────────────────────────────────────────
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(str));
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function initFolderBrowser(accessToken, integrationId) {
|
|
const folderSelector = document.getElementById('folder-selector');
|
|
if (!folderSelector || !integrationId) return;
|
|
|
|
folderSelector.classList.remove('hidden');
|
|
let currentPath = '';
|
|
|
|
const folderList = document.getElementById('folder-list');
|
|
const breadcrumb = document.getElementById('folder-breadcrumb');
|
|
const selectedInput = document.getElementById('selected-folder-path');
|
|
const saveBtn = document.getElementById('save-folder-btn');
|
|
const saveStatus = document.getElementById('folder-save-status');
|
|
|
|
function loadFolders(path) {
|
|
currentPath = path;
|
|
folderList.innerHTML = '<div class="p-4 text-center text-gray-500"><div class="animate-spin inline-block h-5 w-5 border-2 border-gray-300 border-t-indigo-600 rounded-full mb-2" role="status" aria-label="Loading folders"></div><p class="text-sm">Loading folders…</p></div>';
|
|
|
|
const formData = new FormData();
|
|
formData.append('access_token', accessToken);
|
|
formData.append('path', path);
|
|
|
|
fetch('/api/dropbox/list-folders', { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.folders && data.folders.length > 0) {
|
|
folderList.innerHTML = data.folders.map(f =>
|
|
`<button type="button" class="folder-item w-full text-left px-4 py-3 hover:bg-indigo-50 border-b border-gray-100 flex items-center gap-3 text-sm focus:outline-none focus:bg-indigo-50" data-path="${escapeHtml(f.path)}" aria-label="Open folder ${escapeHtml(f.name)}">` +
|
|
`<svg class="h-5 w-5 text-yellow-400 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" /><path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z" /></svg>` +
|
|
`<span class="font-medium text-gray-700">${escapeHtml(f.name)}</span>` +
|
|
`</button>`
|
|
).join('');
|
|
|
|
folderList.querySelectorAll('.folder-item').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const p = btn.getAttribute('data-path');
|
|
selectedInput.value = p;
|
|
loadFolders(p);
|
|
});
|
|
});
|
|
} else {
|
|
folderList.innerHTML = '<div class="p-4 text-center text-gray-400 text-sm">No subfolders found</div>';
|
|
}
|
|
updateBreadcrumb(path);
|
|
})
|
|
.catch(err => {
|
|
folderList.innerHTML = `<div class="p-4 text-center text-red-500 text-sm">Failed to load folders: ${escapeHtml(err.message)}</div>`;
|
|
});
|
|
}
|
|
|
|
function updateBreadcrumb(path) {
|
|
const parts = path.split('/').filter(Boolean);
|
|
let html = '<button type="button" class="folder-nav-btn text-indigo-600 hover:text-indigo-800 font-medium" data-path="" aria-label="Navigate to root folder">/ Root</button>';
|
|
let accumulated = '';
|
|
for (const part of parts) {
|
|
accumulated += '/' + part;
|
|
html += `<span class="mx-1 text-gray-400">/</span>`;
|
|
html += `<button type="button" class="folder-nav-btn text-indigo-600 hover:text-indigo-800 font-medium" data-path="${escapeHtml(accumulated)}" aria-label="Navigate to ${escapeHtml(part)}">${escapeHtml(part)}</button>`;
|
|
}
|
|
breadcrumb.innerHTML = html;
|
|
breadcrumb.querySelectorAll('.folder-nav-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const p = btn.getAttribute('data-path');
|
|
selectedInput.value = p || '/';
|
|
loadFolders(p);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Save selected folder to integration config
|
|
saveBtn.addEventListener('click', () => {
|
|
const folderPath = selectedInput.value.trim() || '/';
|
|
saveBtn.disabled = true;
|
|
saveBtn.textContent = 'Saving…';
|
|
|
|
// Get the current integration config, update folder_path, then PUT back
|
|
fetch(`/api/integrations/${integrationId}`)
|
|
.then(r => r.json())
|
|
.then(intg => {
|
|
const cfg = intg.config || {};
|
|
// Update the correct folder key based on integration type
|
|
if (cfg.source_type) {
|
|
cfg.folder_path = folderPath;
|
|
} else {
|
|
cfg.folder = folderPath;
|
|
}
|
|
return fetch(`/api/integrations/${integrationId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ config: cfg }),
|
|
});
|
|
})
|
|
.then(r => {
|
|
if (!r.ok) throw new Error('Failed to save folder');
|
|
return r.json();
|
|
})
|
|
.then(() => {
|
|
saveStatus.textContent = '✓ Folder saved! Redirecting…';
|
|
saveStatus.className = 'mt-2 text-sm text-green-600';
|
|
saveStatus.classList.remove('hidden');
|
|
saveBtn.textContent = 'Saved ✓';
|
|
setTimeout(() => { window.location.href = '/integrations'; }, 1500);
|
|
})
|
|
.catch(err => {
|
|
saveStatus.textContent = 'Error: ' + err.message;
|
|
saveStatus.className = 'mt-2 text-sm text-red-600';
|
|
saveStatus.classList.remove('hidden');
|
|
saveBtn.disabled = false;
|
|
saveBtn.textContent = 'Save Folder';
|
|
});
|
|
});
|
|
|
|
// Load root folders initially
|
|
loadFolders('');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|