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
+55 -6
View File
@@ -2,33 +2,82 @@
Dropbox integration views for setup and OAuth callback.
"""
from fastapi import Query, Request
import json
from app.views.base import APIRouter, require_login, settings, templates
from fastapi import Query, Request
from sqlalchemy.orm import Session
from app.models import UserIntegration
from app.utils.user_scope import get_current_owner_id
from app.views.base import APIRouter, Depends, get_db, require_login, settings, templates
router = APIRouter()
@router.get("/dropbox-setup")
@require_login
async def dropbox_setup_page(request: Request, integration_id: int | None = Query(None)):
async def dropbox_setup_page(
request: Request,
integration_id: int | None = Query(None),
db: Session = Depends(get_db),
):
"""
Setup page for the Dropbox integration.
Shows configuration status and setup instructions.
When ``integration_id`` is provided the page operates in **user mode**:
the OAuth wizard saves credentials to the named per-user integration
record rather than to the global application settings. Only the folder
path from the integration's existing config is pre-populated; global
admin credentials are never exposed in this mode.
"""
# Check Dropbox configuration
if integration_id is not None:
owner_id = get_current_owner_id(request)
integration = (
db.query(UserIntegration)
.filter(UserIntegration.id == integration_id, UserIntegration.owner_id == owner_id)
.first()
)
if integration:
cfg: dict = {}
if integration.config:
try:
cfg = json.loads(integration.config)
except (json.JSONDecodeError, TypeError):
cfg = {}
# Support both "folder" (DROPBOX destination) and "folder_path" (WATCH_FOLDER source)
folder_path = cfg.get("folder", cfg.get("folder_path", ""))
return templates.TemplateResponse(
"dropbox.html",
{
"request": request,
"user_mode": True,
"is_configured": bool(integration.credentials),
"integration_id": integration_id,
"integration_name": integration.name,
"integration_type": integration.integration_type,
"folder_path": folder_path,
"app_key_value": "",
"app_secret_value": "",
"refresh_token_value": "",
},
)
# ── Admin / global mode ──────────────────────────────────────────────────
is_configured = bool(settings.dropbox_app_key and settings.dropbox_app_secret and settings.dropbox_refresh_token)
return templates.TemplateResponse(
"dropbox.html",
{
"request": request,
"user_mode": False,
"is_configured": is_configured,
"app_key_value": settings.dropbox_app_key or "",
"app_secret_value": settings.dropbox_app_secret if settings.dropbox_app_secret else "",
"refresh_token_value": settings.dropbox_refresh_token if settings.dropbox_refresh_token else "",
"folder_path": settings.dropbox_folder or "/Documents/Uploads", # Default folder path
"folder_path": settings.dropbox_folder or "/Documents/Uploads",
"integration_id": integration_id,
"integration_name": None,
"integration_type": None,
},
)
+56 -11
View File
@@ -2,47 +2,90 @@
Google Drive integration views for setup and OAuth callback.
"""
import json
import urllib.parse
from fastapi import Query, Request
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from app.views.base import APIRouter, require_login, settings, templates
from app.models import UserIntegration
from app.utils.user_scope import get_current_owner_id
from app.views.base import APIRouter, Depends, get_db, require_login, settings, templates
router = APIRouter()
@router.get("/google-drive-setup")
@require_login
async def google_drive_setup_page(request: Request, integration_id: int | None = Query(None)):
async def google_drive_setup_page(
request: Request,
integration_id: int | None = Query(None),
db: Session = Depends(get_db),
):
"""
Setup page for the Google Drive integration.
Shows configuration status and setup instructions.
When ``integration_id`` is provided the page operates in **user mode**:
the OAuth wizard saves credentials to the named per-user integration
record rather than to the global application settings.
"""
# Check if using OAuth
if integration_id is not None:
owner_id = get_current_owner_id(request)
integration = (
db.query(UserIntegration)
.filter(UserIntegration.id == integration_id, UserIntegration.owner_id == owner_id)
.first()
)
if integration:
cfg: dict = {}
if integration.config:
try:
cfg = json.loads(integration.config)
except (json.JSONDecodeError, TypeError):
cfg = {}
folder_id = cfg.get("folder_id", "")
return templates.TemplateResponse(
"google_drive.html",
{
"request": request,
"user_mode": True,
"is_configured": bool(integration.credentials),
"integration_id": integration_id,
"integration_name": integration.name,
"integration_type": integration.integration_type,
"folder_id": folder_id,
"use_oauth": True,
"oauth_configured": bool(integration.credentials),
"sa_configured": False,
"client_id": False,
"client_id_value": "",
"client_secret": False,
"client_secret_value": "",
"refresh_token": False,
"refresh_token_value": "",
"has_credentials_json": False,
},
)
# ── Admin / global mode ──────────────────────────────────────────────────
use_oauth = getattr(settings, "google_drive_use_oauth", False)
# Check Google Drive OAuth configuration
oauth_configured = bool(
settings.google_drive_client_id and settings.google_drive_client_secret and settings.google_drive_refresh_token
)
# Check Google Drive service account configuration
sa_configured = bool(settings.google_drive_credentials_json)
# Overall configuration status
is_configured = (use_oauth and oauth_configured) or (not use_oauth and sa_configured)
if settings.google_drive_folder_id:
is_configured = is_configured and True
else:
is_configured = False
# Get configuration values to display status (hide sensitive values)
return templates.TemplateResponse(
"google_drive.html",
{
"request": request,
"user_mode": False,
"is_configured": is_configured,
"use_oauth": use_oauth,
"oauth_configured": oauth_configured,
@@ -56,6 +99,8 @@ async def google_drive_setup_page(request: Request, integration_id: int | None =
"folder_id": settings.google_drive_folder_id or "",
"has_credentials_json": bool(settings.google_drive_credentials_json),
"integration_id": integration_id,
"integration_name": None,
"integration_type": None,
},
)
+58 -8
View File
@@ -2,40 +2,90 @@
OneDrive integration views for setup and OAuth callback.
"""
from fastapi import Query, Request
import json
from app.views.base import APIRouter, require_login, settings, templates
from fastapi import Query, Request
from sqlalchemy.orm import Session
from app.models import UserIntegration
from app.utils.user_scope import get_current_owner_id
from app.views.base import APIRouter, Depends, get_db, require_login, settings, templates
router = APIRouter()
@router.get("/onedrive-setup")
@require_login
async def onedrive_setup_page(request: Request, integration_id: int | None = Query(None)):
async def onedrive_setup_page(
request: Request,
integration_id: int | None = Query(None),
db: Session = Depends(get_db),
):
"""
Setup page for the OneDrive integration.
Shows configuration status and setup instructions.
When ``integration_id`` is provided the page operates in **user mode**:
the OAuth wizard saves credentials to the named per-user integration
record rather than to the global application settings.
"""
# Check OneDrive configuration
if integration_id is not None:
owner_id = get_current_owner_id(request)
integration = (
db.query(UserIntegration)
.filter(UserIntegration.id == integration_id, UserIntegration.owner_id == owner_id)
.first()
)
if integration:
cfg: dict = {}
if integration.config:
try:
cfg = json.loads(integration.config)
except (json.JSONDecodeError, TypeError):
cfg = {}
# Support both "folder_path" (WATCH_FOLDER / ONEDRIVE destination)
folder_path = cfg.get("folder_path", cfg.get("folder", ""))
return templates.TemplateResponse(
"onedrive.html",
{
"request": request,
"user_mode": True,
"is_configured": bool(integration.credentials),
"integration_id": integration_id,
"integration_name": integration.name,
"integration_type": integration.integration_type,
"folder_path": folder_path,
"client_id": False,
"client_id_value": "",
"client_secret": False,
"client_secret_value": "",
"tenant_id": "common",
"refresh_token": False,
"refresh_token_value": "",
},
)
# ── Admin / global mode ──────────────────────────────────────────────────
is_configured = bool(
settings.onedrive_client_id and settings.onedrive_client_secret and settings.onedrive_refresh_token
)
# Get configuration values to display status (hide sensitive values)
return templates.TemplateResponse(
"onedrive.html",
{
"request": request,
"user_mode": False,
"is_configured": is_configured,
"client_id": bool(settings.onedrive_client_id),
"client_id_value": settings.onedrive_client_id or "", # Pass the actual value for the form
"client_id_value": settings.onedrive_client_id or "",
"client_secret": bool(settings.onedrive_client_secret),
"client_secret_value": settings.onedrive_client_secret if settings.onedrive_client_secret else "",
"tenant_id": settings.onedrive_tenant_id,
"refresh_token": bool(settings.onedrive_refresh_token),
"refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "",
"folder_path": settings.onedrive_folder_path or "Documents/Uploads", # Default folder path
"folder_path": settings.onedrive_folder_path or "Documents/Uploads",
"integration_id": integration_id,
"integration_name": None,
"integration_type": None,
},
)
+71 -14
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 Dropbox
{% 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 Dropbox 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_path %}<br><strong>Folder:</strong> {{ folder_path }}{% 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 Dropbox account.
{% if folder_path %}<br><strong>Target folder:</strong> {{ folder_path }}{% endif %}
</p>
</div>
{% endif %}
{% else %}
<!-- ── Admin-mode header ─────────────────────────────────────────── -->
<h1 class="text-3xl font-bold mb-2">Dropbox Integration Setup</h1>
<p class="text-gray-600 mb-4">
Configure the Dropbox integration for DocuElevate using our setup wizard.
@@ -37,6 +68,7 @@
</div>
</div>
</div>
{% endif %}
</div>
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
@@ -79,24 +111,35 @@
</div>
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">Complete Setup with Wizard</h2>
<h2 class="text-2xl font-semibold mb-4">
{% if user_mode %}OAuth Wizard{% else %}Complete Setup with Wizard{% endif %}
</h2>
<div class="space-y-4">
<div>
<label for="app-key" class="block text-sm font-medium text-gray-700">App Key</label>
<label for="app-key" class="block text-sm font-medium text-gray-700">App Key <span class="text-red-500" aria-hidden="true">*</span></label>
<input type="text" id="app-key" 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 Dropbox app key" value="{{ app_key_value }}">
</div>
<div>
<label for="app-secret" class="block text-sm font-medium text-gray-700">App Secret</label>
<label for="app-secret" class="block text-sm font-medium text-gray-700">App Secret <span class="text-red-500" aria-hidden="true">*</span></label>
<input type="password" id="app-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 Dropbox app secret" value="{{ app_secret_value }}">
</div>
{% if not user_mode %}
<div>
<label for="folder-path" class="block text-sm font-medium text-gray-700">Folder Path (Optional)</label>
<input type="text" id="folder-path" 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="/Documents/Uploads" value="{{ folder_path }}">
<p class="text-xs text-gray-500 mt-1">Enter the folder path where files should be uploaded (e.g., /Documents/Uploads)</p>
</div>
{% else %}
{% if folder_path %}
<div class="bg-gray-50 border border-gray-200 rounded-md px-3 py-2">
<p class="text-xs text-gray-500">Target folder (from integration settings)</p>
<p class="text-sm font-mono text-gray-700">{{ folder_path }}</p>
</div>
{% endif %}
{% endif %}
<div>
<button id="start-auth-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">
@@ -104,7 +147,8 @@
</button>
</div>
<!-- Token validation and status -->
<!-- Token validation and status (admin mode only) -->
{% if not user_mode %}
<div id="token-status" class="mt-6 {{ 'hidden' if not is_configured else '' }}">
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
<div class="flex">
@@ -140,7 +184,7 @@
</button>
</div>
<!-- Configuration for Worker Nodes section -->
<!-- Configuration for Worker Nodes section (admin only) -->
<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">
@@ -163,6 +207,7 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
</p>
</div>
</div>
{% endif %}
</div>
</div>
@@ -184,9 +229,15 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
</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 -->
@@ -216,6 +267,8 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
{% 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) {
@@ -279,7 +332,6 @@ document.addEventListener('DOMContentLoaded', function() {
startAuthFlowBtn.addEventListener('click', function() {
const appKey = document.getElementById('app-key').value.trim();
const appSecret = appSecretInput.value.trim();
const folderPath = document.getElementById('folder-path').value.trim();
const redirectUri = window.location.origin + "/dropbox-callback";
if (!appKey) {
@@ -292,11 +344,16 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
// Save app key, app secret and folder path to session storage temporarily
// Save app key and app secret to session storage temporarily
sessionStorage.setItem('dropbox_app_key', appKey);
sessionStorage.setItem('dropbox_app_secret', appSecret);
if (folderPath) {
sessionStorage.setItem('dropbox_folder_path', folderPath);
// In admin mode also store folder path; in user mode the config is already set
if (!userMode) {
const folderPathEl = document.getElementById('folder-path');
if (folderPathEl && folderPathEl.value.trim()) {
sessionStorage.setItem('dropbox_folder_path', folderPathEl.value.trim());
}
}
// Generate the authorization URL
@@ -306,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = authUrl;
});
// Test Token button click
// Test Token button click (admin mode only)
if (testTokenBtn) {
testTokenBtn.addEventListener('click', function() {
testTokenBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
@@ -343,7 +400,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Refresh Token button click
// Refresh Token button click (admin mode only)
if (refreshTokenBtn) {
refreshTokenBtn.addEventListener('click', function() {
showModal('info', 'Confirm', 'This will start a new authentication flow to obtain a fresh token from Dropbox. Continue?');
@@ -374,7 +431,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Copy Environment Variables Button
// Copy Environment Variables Button (admin mode only)
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
copyEnvVarsBtn.addEventListener('click', function() {
@@ -403,8 +460,8 @@ document.addEventListener('DOMContentLoaded', function() {
sessionStorage.removeItem('dropbox_app_secret');
}
// If token is not configured but we have an app key, show the token status section
if (document.getElementById('app-key').value && !tokenStatus.classList.contains('hidden')) {
// If token is not configured but we have an app key, show the token status section (admin mode)
if (tokenStatus && document.getElementById('app-key').value && !tokenStatus.classList.contains('hidden')) {
tokenStatus.classList.remove('hidden');
}
});
+2 -3
View File
@@ -145,11 +145,10 @@ document.addEventListener('DOMContentLoaded', function() {
app_key: appKey,
app_secret: appSecret,
};
const cfgUpdate = {};
if (folderPath) cfgUpdate.folder = folderPath;
// Only send credentials — the integration's config (folder, source_type, etc.)
// is already set and must not be overwritten here.
const body = { credentials: creds };
if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate;
document.getElementById('processing-message').innerHTML =
'<p>Saving credentials to your integration...</p>';
+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');
@@ -200,11 +200,10 @@ document.addEventListener('DOMContentLoaded', function() {
client_secret: clientSecret,
refresh_token: data.refresh_token,
};
const cfgUpdate = {};
if (folderId) cfgUpdate.folder_id = folderId;
// Only send credentials — the integration's config (folder_id, etc.)
// is already set and must not be overwritten here.
const body = { credentials: creds };
if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate;
document.getElementById('processing-message').innerHTML =
'<p>Saving credentials to your integration...</p>';
+36 -12
View File
@@ -206,14 +206,17 @@
</div>
<!-- Actions -->
<div class="flex items-center gap-2 shrink-0">
<template x-if="isOAuthType(intg.integration_type) && !intg.has_credentials">
<template x-if="isOAuthType(intg)">
<a
:href="oauthLink(intg.integration_type) + '?integration_id=' + intg.id"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-yellow-400 text-yellow-700 bg-yellow-50 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-yellow-500"
:href="oauthLink(intg) + '?integration_id=' + intg.id"
:class="intg.has_credentials
? 'inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-blue-300 text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500'
: 'inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-yellow-400 text-yellow-700 bg-yellow-50 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-yellow-500'"
style="min-height:36px;"
:aria-label="`Authorize ${intg.name}`"
:aria-label="`${intg.has_credentials ? 'Re-authorize' : 'Authorize'} ${intg.name}`"
>
<i class="fas fa-key mr-1" aria-hidden="true"></i>Authorize
<i class="fas fa-key mr-1" aria-hidden="true"></i>
<span x-text="intg.has_credentials ? 'Re-Authorize' : 'Authorize'"></span>
</a>
</template>
<button
@@ -289,14 +292,17 @@
</div>
<!-- Actions -->
<div class="flex items-center gap-2 shrink-0">
<template x-if="isOAuthType(intg.integration_type) && !intg.has_credentials">
<template x-if="isOAuthType(intg)">
<a
:href="oauthLink(intg.integration_type) + '?integration_id=' + intg.id"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-yellow-400 text-yellow-700 bg-yellow-50 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-yellow-500"
:href="oauthLink(intg) + '?integration_id=' + intg.id"
:class="intg.has_credentials
? 'inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-blue-300 text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500'
: 'inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-yellow-400 text-yellow-700 bg-yellow-50 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-yellow-500'"
style="min-height:36px;"
:aria-label="`Authorize ${intg.name}`"
:aria-label="`${intg.has_credentials ? 'Re-authorize' : 'Authorize'} ${intg.name}`"
>
<i class="fas fa-key mr-1" aria-hidden="true"></i>Authorize
<i class="fas fa-key mr-1" aria-hidden="true"></i>
<span x-text="intg.has_credentials ? 'Re-Authorize' : 'Authorize'"></span>
</a>
</template>
<button
@@ -1133,12 +1139,30 @@ function integrationsDashboard() {
typeLabel(t) { return TYPE_LABELS[t] || t; },
typeIcon(t) { return TYPE_ICONS[t] || 'fa-plug text-gray-400'; },
hasFormFields(t) { return TYPES_WITH_FORM_FIELDS.has(t); },
isOAuthType(t) { return OAUTH_TYPES.has(t); },
isOAuthType(intg) {
// Direct OAuth types: DROPBOX, GOOGLE_DRIVE, ONEDRIVE
if (OAUTH_TYPES.has(intg.integration_type)) return true;
// WATCH_FOLDER with an OAuth-backed source type (dropbox, onedrive, google_drive)
if (intg.integration_type === 'WATCH_FOLDER') {
const cfg = intg.config || {};
const src = (cfg.source_type || '').toLowerCase();
return src === 'dropbox' || src === 'onedrive' || src === 'google_drive';
}
return false;
},
oauthLink(t) {
oauthLink(intg) {
const t = intg.integration_type;
if (t === 'DROPBOX') return '/dropbox-setup';
if (t === 'GOOGLE_DRIVE') return '/google-drive-setup';
if (t === 'ONEDRIVE') return '/onedrive-setup';
if (t === 'WATCH_FOLDER') {
const cfg = intg.config || {};
const src = (cfg.source_type || '').toLowerCase();
if (src === 'dropbox') return '/dropbox-setup';
if (src === 'onedrive') return '/onedrive-setup';
if (src === 'google_drive') return '/google-drive-setup';
}
return '#';
},
+69 -19
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 OneDrive
{% 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 OneDrive 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_path %}<br><strong>Folder:</strong> {{ folder_path }}{% 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 OneDrive account.
{% if folder_path %}<br><strong>Target folder:</strong> {{ folder_path }}{% endif %}
</p>
</div>
{% endif %}
{% else %}
<!-- ── Admin-mode header ─────────────────────────────────────────── -->
<h1 class="text-3xl font-bold mb-2">OneDrive Integration Setup</h1>
<p class="text-gray-600 mb-4">
Configure the Microsoft OneDrive integration for DocuElevate using our setup wizard.
@@ -37,6 +68,7 @@
</div>
</div>
</div>
{% endif %}
</div>
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
@@ -93,16 +125,18 @@
</div>
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">Complete Setup with Wizard</h2>
<h2 class="text-2xl font-semibold mb-4">
{% if user_mode %}OAuth Wizard{% else %}Complete Setup with Wizard{% endif %}
</h2>
<div class="space-y-4">
<div>
<label for="client-id" class="block text-sm font-medium text-gray-700">Client ID</label>
<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 Azure AD application client ID" value="{{ client_id_value }}">
</div>
<div>
<label for="client-secret" class="block text-sm font-medium text-gray-700">Client Secret</label>
<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 Azure AD application client secret" value="{{ client_secret_value }}">
</div>
@@ -112,11 +146,20 @@
<p class="text-xs text-gray-500 mt-1">Use "common" for personal accounts or your organization's Tenant ID for corporate accounts</p>
</div>
{% if not user_mode %}
<div>
<label for="folder-path" class="block text-sm font-medium text-gray-700">Folder Path (Optional)</label>
<input type="text" id="folder-path" 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="Documents/Uploads" value="{{ folder_path }}">
<p class="text-xs text-gray-500 mt-1">Enter the folder path where files should be uploaded (e.g., Documents/Uploads)</p>
</div>
{% else %}
{% if folder_path %}
<div class="bg-gray-50 border border-gray-200 rounded-md px-3 py-2">
<p class="text-xs text-gray-500">Target folder (from integration settings)</p>
<p class="text-sm font-mono text-gray-700">{{ folder_path }}</p>
</div>
{% endif %}
{% endif %}
<div>
<button id="start-auth-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">
@@ -124,7 +167,8 @@
</button>
</div>
<!-- Token validation and status -->
<!-- Token validation and status (admin mode only) -->
{% if not user_mode %}
<div id="token-status" class="mt-6 {{ 'hidden' if not is_configured else '' }}">
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
<div class="flex">
@@ -160,7 +204,7 @@
</button>
</div>
<!-- Configuration for Worker Nodes section -->
<!-- Configuration for Worker Nodes section (admin only) -->
<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">
@@ -184,6 +228,7 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
</p>
</div>
</div>
{% endif %}
</div>
</div>
@@ -205,9 +250,15 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
</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 -->
@@ -237,6 +288,8 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
{% 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) {
@@ -302,7 +355,6 @@ document.addEventListener('DOMContentLoaded', function() {
const clientSecret = clientSecretInput.value.trim();
const redirectUri = window.location.origin + "/onedrive-callback";
const tenantId = document.getElementById('tenant-id').value.trim() || 'common';
const folderPath = document.getElementById('folder-path') ? document.getElementById('folder-path').value.trim() : '';
if (!clientId) {
showModal('error', 'Validation Error', 'Please enter your Client ID');
@@ -319,8 +371,12 @@ document.addEventListener('DOMContentLoaded', function() {
sessionStorage.setItem('onedrive_client_secret', clientSecret);
sessionStorage.setItem('onedrive_tenant_id', tenantId);
if (folderPath) {
sessionStorage.setItem('onedrive_folder_path', folderPath);
// In admin mode also store folder path; in user mode the config is already set
if (!userMode) {
const folderPathEl = document.getElementById('folder-path');
if (folderPathEl && folderPathEl.value.trim()) {
sessionStorage.setItem('onedrive_folder_path', folderPathEl.value.trim());
}
}
// Generate the authorization URL with .default scope
@@ -330,7 +386,7 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = authUrl;
});
// Test Token button click
// Test Token button click (admin mode only)
if (testTokenBtn) {
testTokenBtn.addEventListener('click', function() {
testTokenBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
@@ -367,7 +423,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Refresh Token button click
// Refresh Token button click (admin mode only)
if (refreshTokenBtn) {
refreshTokenBtn.addEventListener('click', function() {
showModal('info', 'Confirm', 'This will start a new authentication flow to obtain a fresh token from Microsoft. Continue?');
@@ -398,7 +454,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Copy Environment Variables Button
// Copy Environment Variables Button (admin mode only)
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
copyEnvVarsBtn.addEventListener('click', function() {
@@ -437,14 +493,8 @@ document.addEventListener('DOMContentLoaded', function() {
tenantIdInput.value = sessionStorage.getItem('onedrive_tenant_id');
}
// Check for folder path in session storage
const folderPathInput = document.getElementById('folder-path');
if (folderPathInput && !folderPathInput.value && sessionStorage.getItem('onedrive_folder_path')) {
folderPathInput.value = sessionStorage.getItem('onedrive_folder_path');
}
// If token is not configured but we have a client ID, show the token status section
if (document.getElementById('client-id').value && !tokenStatus.classList.contains('hidden')) {
// If token is configured and we have a client ID, show the token status section (admin mode)
if (tokenStatus && document.getElementById('client-id').value && !tokenStatus.classList.contains('hidden')) {
tokenStatus.classList.remove('hidden');
}
});
+2 -3
View File
@@ -149,11 +149,10 @@ document.addEventListener('DOMContentLoaded', function() {
refresh_token: data.refresh_token,
tenant_id: tenantId,
};
const cfgUpdate = {};
if (folderPath) cfgUpdate.folder_path = folderPath;
// Only send credentials — the integration's config (folder_path, source_type, etc.)
// is already set and must not be overwritten here.
const body = { credentials: creds };
if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate;
document.getElementById('processing-message').innerHTML =
'<p>Saving credentials to your integration...</p>';