Merge pull request #558 from christianlouis/copilot/fix-user-specific-tokens
feat(auth): make OAuth credentials user-specific via UserIntegration records
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
Dropbox integration views for setup and OAuth callback.
|
Dropbox integration views for setup and OAuth callback.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Query, Request
|
||||||
|
|
||||||
from app.views.base import APIRouter, require_login, settings, templates
|
from app.views.base import APIRouter, require_login, settings, templates
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get("/dropbox-setup")
|
@router.get("/dropbox-setup")
|
||||||
@require_login
|
@require_login
|
||||||
async def dropbox_setup_page(request: Request):
|
async def dropbox_setup_page(request: Request, integration_id: int | None = Query(None)):
|
||||||
"""
|
"""
|
||||||
Setup page for the Dropbox integration.
|
Setup page for the Dropbox integration.
|
||||||
Shows configuration status and setup instructions.
|
Shows configuration status and setup instructions.
|
||||||
@@ -28,6 +28,7 @@ async def dropbox_setup_page(request: Request):
|
|||||||
"app_secret_value": settings.dropbox_app_secret if settings.dropbox_app_secret else "",
|
"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 "",
|
"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", # Default folder path
|
||||||
|
"integration_id": integration_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Google Drive integration views for setup and OAuth callback.
|
|||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Query, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from app.views.base import APIRouter, require_login, settings, templates
|
from app.views.base import APIRouter, require_login, settings, templates
|
||||||
@@ -14,7 +14,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get("/google-drive-setup")
|
@router.get("/google-drive-setup")
|
||||||
@require_login
|
@require_login
|
||||||
async def google_drive_setup_page(request: Request):
|
async def google_drive_setup_page(request: Request, integration_id: int | None = Query(None)):
|
||||||
"""
|
"""
|
||||||
Setup page for the Google Drive integration.
|
Setup page for the Google Drive integration.
|
||||||
Shows configuration status and setup instructions.
|
Shows configuration status and setup instructions.
|
||||||
@@ -55,6 +55,7 @@ async def google_drive_setup_page(request: Request):
|
|||||||
"refresh_token_value": settings.google_drive_refresh_token or "",
|
"refresh_token_value": settings.google_drive_refresh_token or "",
|
||||||
"folder_id": settings.google_drive_folder_id or "",
|
"folder_id": settings.google_drive_folder_id or "",
|
||||||
"has_credentials_json": bool(settings.google_drive_credentials_json),
|
"has_credentials_json": bool(settings.google_drive_credentials_json),
|
||||||
|
"integration_id": integration_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
OneDrive integration views for setup and OAuth callback.
|
OneDrive integration views for setup and OAuth callback.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Query, Request
|
||||||
|
|
||||||
from app.views.base import APIRouter, require_login, settings, templates
|
from app.views.base import APIRouter, require_login, settings, templates
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get("/onedrive-setup")
|
@router.get("/onedrive-setup")
|
||||||
@require_login
|
@require_login
|
||||||
async def onedrive_setup_page(request: Request):
|
async def onedrive_setup_page(request: Request, integration_id: int | None = Query(None)):
|
||||||
"""
|
"""
|
||||||
Setup page for the OneDrive integration.
|
Setup page for the OneDrive integration.
|
||||||
Shows configuration status and setup instructions.
|
Shows configuration status and setup instructions.
|
||||||
@@ -35,6 +35,7 @@ async def onedrive_setup_page(request: Request):
|
|||||||
"refresh_token": bool(settings.onedrive_refresh_token),
|
"refresh_token": bool(settings.onedrive_refresh_token),
|
||||||
"refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "",
|
"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", # Default folder path
|
||||||
|
"integration_id": integration_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,12 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Store integration_id if provided (for per-user OAuth flow)
|
||||||
|
const integrationId = "{{ integration_id or '' }}";
|
||||||
|
if (integrationId) {
|
||||||
|
sessionStorage.setItem('oauth_integration_id', integrationId);
|
||||||
|
}
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
const startAuthFlowBtn = document.getElementById('start-auth-flow');
|
const startAuthFlowBtn = document.getElementById('start-auth-flow');
|
||||||
const testTokenBtn = document.getElementById('test-token');
|
const testTokenBtn = document.getElementById('test-token');
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const appKey = sessionStorage.getItem('dropbox_app_key') || "{{ app_key_value }}";
|
const appKey = sessionStorage.getItem('dropbox_app_key') || "{{ app_key_value }}";
|
||||||
const appSecret = sessionStorage.getItem('dropbox_app_secret') || "{{ app_secret_value }}";
|
const appSecret = sessionStorage.getItem('dropbox_app_secret') || "{{ app_secret_value }}";
|
||||||
const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads';
|
const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads';
|
||||||
|
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
||||||
|
|
||||||
const redirectUri = window.location.origin + "/dropbox-callback";
|
const redirectUri = window.location.origin + "/dropbox-callback";
|
||||||
|
|
||||||
@@ -137,7 +138,50 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.refresh_token) {
|
if (data.refresh_token) {
|
||||||
// Update settings in memory
|
// 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,
|
||||||
|
};
|
||||||
|
const cfgUpdate = {};
|
||||||
|
if (folderPath) cfgUpdate.folder = folderPath;
|
||||||
|
|
||||||
|
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>';
|
||||||
|
|
||||||
|
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
|
||||||
|
sessionStorage.removeItem('dropbox_app_key');
|
||||||
|
sessionStorage.removeItem('dropbox_app_secret');
|
||||||
|
sessionStorage.removeItem('dropbox_folder_path');
|
||||||
|
sessionStorage.removeItem('oauth_integration_id');
|
||||||
|
|
||||||
|
// Show brief success then redirect to integrations
|
||||||
|
document.getElementById('processing-message').innerHTML =
|
||||||
|
'<p class="text-green-600 font-semibold">✓ Dropbox authorized successfully!</p>' +
|
||||||
|
'<p class="text-sm text-gray-500 mt-2">Redirecting to Integrations...</p>';
|
||||||
|
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||||
|
setTimeout(() => { window.location.href = '/integrations'; }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global flow: update settings in memory
|
||||||
const updateFormData = new FormData();
|
const updateFormData = new FormData();
|
||||||
updateFormData.append('refresh_token', data.refresh_token);
|
updateFormData.append('refresh_token', data.refresh_token);
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,12 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 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
|
// Tab elements
|
||||||
const oauthTabBtn = document.getElementById('oauth-tab-btn');
|
const oauthTabBtn = document.getElementById('oauth-tab-btn');
|
||||||
const saTabBtn = document.getElementById('sa-tab-btn');
|
const saTabBtn = document.getElementById('sa-tab-btn');
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const clientId = sessionStorage.getItem('google_drive_client_id');
|
const clientId = sessionStorage.getItem('google_drive_client_id');
|
||||||
const clientSecret = sessionStorage.getItem('google_drive_client_secret');
|
const clientSecret = sessionStorage.getItem('google_drive_client_secret');
|
||||||
const folderId = sessionStorage.getItem('google_drive_folder_id');
|
const folderId = sessionStorage.getItem('google_drive_folder_id');
|
||||||
|
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
||||||
|
|
||||||
const redirectUri = window.location.origin + "/google-drive-callback";
|
const redirectUri = window.location.origin + "/google-drive-callback";
|
||||||
|
|
||||||
@@ -192,7 +193,51 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
refreshToken = data.refresh_token;
|
refreshToken = data.refresh_token;
|
||||||
accessToken = data.access_token;
|
accessToken = data.access_token;
|
||||||
|
|
||||||
// Update settings in memory first
|
// Per-user flow: save to the user's integration record
|
||||||
|
if (integrationId) {
|
||||||
|
const creds = {
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
refresh_token: data.refresh_token,
|
||||||
|
};
|
||||||
|
const cfgUpdate = {};
|
||||||
|
if (folderId) cfgUpdate.folder_id = folderId;
|
||||||
|
|
||||||
|
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>';
|
||||||
|
|
||||||
|
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
|
||||||
|
sessionStorage.removeItem('google_drive_client_id');
|
||||||
|
sessionStorage.removeItem('google_drive_client_secret');
|
||||||
|
sessionStorage.removeItem('google_drive_folder_id');
|
||||||
|
sessionStorage.removeItem('google_drive_use_oauth');
|
||||||
|
sessionStorage.removeItem('oauth_integration_id');
|
||||||
|
|
||||||
|
// Show brief success then redirect to integrations
|
||||||
|
document.getElementById('processing-message').innerHTML =
|
||||||
|
'<p class="text-green-600 font-semibold">✓ Google Drive authorized successfully!</p>' +
|
||||||
|
'<p class="text-sm text-gray-500 mt-2">Redirecting to Integrations...</p>';
|
||||||
|
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||||
|
setTimeout(() => { window.location.href = '/integrations'; }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global flow: update settings in memory first
|
||||||
const updateFormData = new FormData();
|
const updateFormData = new FormData();
|
||||||
updateFormData.append('refresh_token', data.refresh_token);
|
updateFormData.append('refresh_token', data.refresh_token);
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<template x-if="isOAuthType(intg.integration_type) && !intg.has_credentials">
|
||||||
|
<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"
|
||||||
|
style="min-height:36px;"
|
||||||
|
:aria-label="`Authorize ${intg.name}`"
|
||||||
|
>
|
||||||
|
<i class="fas fa-key mr-1" aria-hidden="true"></i>Authorize
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="testSavedIntegration(intg)"
|
@click="testSavedIntegration(intg)"
|
||||||
@@ -279,6 +289,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<template x-if="isOAuthType(intg.integration_type) && !intg.has_credentials">
|
||||||
|
<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"
|
||||||
|
style="min-height:36px;"
|
||||||
|
:aria-label="`Authorize ${intg.name}`"
|
||||||
|
>
|
||||||
|
<i class="fas fa-key mr-1" aria-hidden="true"></i>Authorize
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="testSavedIntegration(intg)"
|
@click="testSavedIntegration(intg)"
|
||||||
@@ -561,9 +581,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bg-blue-50 dark:bg-blue-900/30 rounded-md p-3 text-sm text-blue-700 dark:text-blue-300">
|
<div class="bg-blue-50 dark:bg-blue-900/30 rounded-md p-3 text-sm text-blue-700 dark:text-blue-300">
|
||||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||||
OAuth tokens can be configured via the dedicated
|
Save this integration first, then use the <strong>Authorize</strong> button to complete the OAuth flow.
|
||||||
<a :href="oauthLink(form.integration_type)" class="underline hover:text-blue-900 dark:hover:text-blue-100" x-text="typeLabel(form.integration_type) + ' setup page'"></a>.
|
Your credentials will be stored securely in your personal integration record.
|
||||||
Once authorised, the token is stored automatically.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -742,6 +761,7 @@ function integrationsDashboard() {
|
|||||||
const SOURCE_TYPES = ['IMAP', 'WATCH_FOLDER', 'WEBHOOK'];
|
const SOURCE_TYPES = ['IMAP', 'WATCH_FOLDER', 'WEBHOOK'];
|
||||||
const DEST_TYPES = ['S3', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'EMAIL', 'PAPERLESS', 'RCLONE'];
|
const DEST_TYPES = ['S3', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'EMAIL', 'PAPERLESS', 'RCLONE'];
|
||||||
const TYPES_WITH_FORM_FIELDS = new Set(['IMAP', 'S3', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'EMAIL', 'WATCH_FOLDER', 'PAPERLESS']);
|
const TYPES_WITH_FORM_FIELDS = new Set(['IMAP', 'S3', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'EMAIL', 'WATCH_FOLDER', 'PAPERLESS']);
|
||||||
|
const OAUTH_TYPES = new Set(['DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE']);
|
||||||
|
|
||||||
const TYPE_LABELS = {
|
const TYPE_LABELS = {
|
||||||
IMAP: 'IMAP Email',
|
IMAP: 'IMAP Email',
|
||||||
@@ -838,11 +858,12 @@ function integrationsDashboard() {
|
|||||||
typeLabel(t) { return TYPE_LABELS[t] || t; },
|
typeLabel(t) { return TYPE_LABELS[t] || t; },
|
||||||
typeIcon(t) { return TYPE_ICONS[t] || 'fa-plug text-gray-400'; },
|
typeIcon(t) { return TYPE_ICONS[t] || 'fa-plug text-gray-400'; },
|
||||||
hasFormFields(t) { return TYPES_WITH_FORM_FIELDS.has(t); },
|
hasFormFields(t) { return TYPES_WITH_FORM_FIELDS.has(t); },
|
||||||
|
isOAuthType(t) { return OAUTH_TYPES.has(t); },
|
||||||
|
|
||||||
oauthLink(t) {
|
oauthLink(t) {
|
||||||
if (t === 'DROPBOX') return '/dropbox';
|
if (t === 'DROPBOX') return '/dropbox-setup';
|
||||||
if (t === 'GOOGLE_DRIVE') return '/google-drive';
|
if (t === 'GOOGLE_DRIVE') return '/google-drive-setup';
|
||||||
if (t === 'ONEDRIVE') return '/onedrive';
|
if (t === 'ONEDRIVE') return '/onedrive-setup';
|
||||||
return '#';
|
return '#';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,12 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Store integration_id if provided (for per-user OAuth flow)
|
||||||
|
const integrationId = "{{ integration_id or '' }}";
|
||||||
|
if (integrationId) {
|
||||||
|
sessionStorage.setItem('oauth_integration_id', integrationId);
|
||||||
|
}
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
const startAuthFlowBtn = document.getElementById('start-auth-flow');
|
const startAuthFlowBtn = document.getElementById('start-auth-flow');
|
||||||
const testTokenBtn = document.getElementById('test-token');
|
const testTokenBtn = document.getElementById('test-token');
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const clientSecret = sessionStorage.getItem('onedrive_client_secret') || "{{ client_secret_value }}";
|
const clientSecret = sessionStorage.getItem('onedrive_client_secret') || "{{ client_secret_value }}";
|
||||||
const tenantId = sessionStorage.getItem('onedrive_tenant_id') || "{{ tenant_id }}" || "common";
|
const tenantId = sessionStorage.getItem('onedrive_tenant_id') || "{{ tenant_id }}" || "common";
|
||||||
const folderPath = sessionStorage.getItem('onedrive_folder_path') || "";
|
const folderPath = sessionStorage.getItem('onedrive_folder_path') || "";
|
||||||
|
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
||||||
|
|
||||||
const redirectUri = window.location.origin + "/onedrive-callback";
|
const redirectUri = window.location.origin + "/onedrive-callback";
|
||||||
|
|
||||||
@@ -140,7 +141,52 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.refresh_token) {
|
if (data.refresh_token) {
|
||||||
// Instead of saving to .env file, update settings in memory
|
// Per-user flow: save to the user's integration record
|
||||||
|
if (integrationId) {
|
||||||
|
const creds = {
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
refresh_token: data.refresh_token,
|
||||||
|
tenant_id: tenantId,
|
||||||
|
};
|
||||||
|
const cfgUpdate = {};
|
||||||
|
if (folderPath) cfgUpdate.folder_path = folderPath;
|
||||||
|
|
||||||
|
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>';
|
||||||
|
|
||||||
|
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
|
||||||
|
sessionStorage.removeItem('onedrive_client_id');
|
||||||
|
sessionStorage.removeItem('onedrive_client_secret');
|
||||||
|
sessionStorage.removeItem('onedrive_tenant_id');
|
||||||
|
sessionStorage.removeItem('onedrive_folder_path');
|
||||||
|
sessionStorage.removeItem('oauth_integration_id');
|
||||||
|
|
||||||
|
// Show brief success then redirect to integrations
|
||||||
|
document.getElementById('processing-message').innerHTML =
|
||||||
|
'<p class="text-green-600 font-semibold">✓ OneDrive authorized successfully!</p>' +
|
||||||
|
'<p class="text-sm text-gray-500 mt-2">Redirecting to Integrations...</p>';
|
||||||
|
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||||
|
setTimeout(() => { window.location.href = '/integrations'; }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global flow: update settings in memory
|
||||||
const updateFormData = new FormData();
|
const updateFormData = new FormData();
|
||||||
updateFormData.append('refresh_token', data.refresh_token);
|
updateFormData.append('refresh_token', data.refresh_token);
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,21 @@ class TestDropboxViews:
|
|||||||
"""Test the Dropbox OAuth callback with auth code."""
|
"""Test the Dropbox OAuth callback with auth code."""
|
||||||
response = client.get("/dropbox-callback?code=test_code")
|
response = client.get("/dropbox-callback?code=test_code")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_dropbox_setup_page_with_integration_id(self, client):
|
||||||
|
"""Test the Dropbox setup page accepts integration_id query param."""
|
||||||
|
response = client.get("/dropbox-setup?integration_id=42")
|
||||||
|
assert response.status_code == 200
|
||||||
|
# The template should store the integration_id for per-user OAuth flow
|
||||||
|
assert b"oauth_integration_id" in response.content
|
||||||
|
assert b"42" in response.content
|
||||||
|
|
||||||
|
def test_dropbox_setup_page_without_integration_id(self, client):
|
||||||
|
"""Test the Dropbox setup page works without integration_id (global flow)."""
|
||||||
|
response = client.get("/dropbox-setup")
|
||||||
|
assert response.status_code == 200
|
||||||
|
# The template should not set integration_id when not provided
|
||||||
|
body = response.text
|
||||||
|
assert "oauth_integration_id" in body # The JS code is always present
|
||||||
|
# But integration_id template var should be empty
|
||||||
|
assert 'const integrationId = ""' in body
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ class TestGoogleDriveViews:
|
|||||||
response = client.get("/google-drive-callback?code=test_code")
|
response = client.get("/google-drive-callback?code=test_code")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_google_drive_setup_page_with_integration_id(self, client):
|
||||||
|
"""Test the Google Drive setup page accepts integration_id query param."""
|
||||||
|
response = client.get("/google-drive-setup?integration_id=99")
|
||||||
|
assert response.status_code == 200
|
||||||
|
# The template should store the integration_id for per-user OAuth flow
|
||||||
|
assert b"oauth_integration_id" in response.content
|
||||||
|
assert b"99" in response.content
|
||||||
|
|
||||||
|
def test_google_drive_setup_page_without_integration_id(self, client):
|
||||||
|
"""Test the Google Drive setup page works without integration_id (global flow)."""
|
||||||
|
response = client.get("/google-drive-setup")
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.text
|
||||||
|
assert "oauth_integration_id" in body
|
||||||
|
assert 'const integrationId = ""' in body
|
||||||
|
|
||||||
def test_google_drive_callback_with_code_and_state(self, client):
|
def test_google_drive_callback_with_code_and_state(self, client):
|
||||||
"""Test the Google Drive OAuth callback with code and state."""
|
"""Test the Google Drive OAuth callback with code and state."""
|
||||||
response = client.get("/google-drive-callback?code=test_code&state=test_state")
|
response = client.get("/google-drive-callback?code=test_code&state=test_state")
|
||||||
|
|||||||
@@ -63,6 +63,46 @@ class TestIntegrationsDashboardView:
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"integrationsDashboard" in response.content
|
assert b"integrationsDashboard" in response.content
|
||||||
|
|
||||||
|
def test_integrations_page_oauth_links_point_to_setup_pages(self, client):
|
||||||
|
"""oauthLink() must reference the correct -setup URLs, not bare paths."""
|
||||||
|
response = client.get("/integrations")
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.text
|
||||||
|
assert "'/dropbox-setup'" in body or '"/dropbox-setup"' in body
|
||||||
|
assert "'/google-drive-setup'" in body or '"/google-drive-setup"' in body
|
||||||
|
assert "'/onedrive-setup'" in body or '"/onedrive-setup"' in body
|
||||||
|
# Ensure the old broken paths are gone
|
||||||
|
assert "return '/dropbox'" not in body.replace("/dropbox-setup", "")
|
||||||
|
assert "return '/google-drive'" not in body.replace("/google-drive-setup", "")
|
||||||
|
assert "return '/onedrive'" not in body.replace("/onedrive-setup", "")
|
||||||
|
|
||||||
|
def test_integrations_page_contains_authorize_button(self, client):
|
||||||
|
"""GET /integrations page includes per-user Authorize button for OAuth types."""
|
||||||
|
response = client.get("/integrations")
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.text
|
||||||
|
assert "isOAuthType" in body
|
||||||
|
assert "Authorize" in body
|
||||||
|
assert "integration_id=" in body
|
||||||
|
|
||||||
|
def test_integrations_page_contains_oauth_types_constant(self, client):
|
||||||
|
"""GET /integrations includes OAUTH_TYPES Set constant."""
|
||||||
|
response = client.get("/integrations")
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.text
|
||||||
|
assert "OAUTH_TYPES" in body
|
||||||
|
assert "'DROPBOX'" in body
|
||||||
|
assert "'GOOGLE_DRIVE'" in body
|
||||||
|
assert "'ONEDRIVE'" in body
|
||||||
|
|
||||||
|
def test_integrations_page_modal_info_box_per_user(self, client):
|
||||||
|
"""The create modal info box instructs to save first then authorize."""
|
||||||
|
response = client.get("/integrations")
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.text
|
||||||
|
assert "Save this integration first" in body
|
||||||
|
assert "personal integration record" in body
|
||||||
|
|
||||||
def test_integrations_page_contains_quota_indicators(self, client):
|
def test_integrations_page_contains_quota_indicators(self, client):
|
||||||
"""GET /integrations page includes quota labels."""
|
"""GET /integrations page includes quota labels."""
|
||||||
response = client.get("/integrations")
|
response = client.get("/integrations")
|
||||||
|
|||||||
@@ -26,3 +26,19 @@ class TestOnedriveViews:
|
|||||||
"""Test the OneDrive OAuth callback with auth code."""
|
"""Test the OneDrive OAuth callback with auth code."""
|
||||||
response = client.get("/onedrive-callback?code=test_code")
|
response = client.get("/onedrive-callback?code=test_code")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_onedrive_setup_page_with_integration_id(self, client):
|
||||||
|
"""Test the OneDrive setup page accepts integration_id query param."""
|
||||||
|
response = client.get("/onedrive-setup?integration_id=77")
|
||||||
|
assert response.status_code == 200
|
||||||
|
# The template should store the integration_id for per-user OAuth flow
|
||||||
|
assert b"oauth_integration_id" in response.content
|
||||||
|
assert b"77" in response.content
|
||||||
|
|
||||||
|
def test_onedrive_setup_page_without_integration_id(self, client):
|
||||||
|
"""Test the OneDrive setup page works without integration_id (global flow)."""
|
||||||
|
response = client.get("/onedrive-setup")
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.text
|
||||||
|
assert "oauth_integration_id" in body
|
||||||
|
assert 'const integrationId = ""' in body
|
||||||
|
|||||||
Reference in New Issue
Block a user