From 89f5b74b4fee4900bfe9d09bb4fca17a7ba945a8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Apr 2026 02:42:29 +0000
Subject: [PATCH 1/4] Fix auth persistence and OAuth popup flow
Agent-Logs-Url: https://github.com/crazyrob425/BlacklistedAIProxy/sessions/63e61e1c-0502-4d15-91ef-0b43d3ac4c84
Co-authored-by: crazyrob425 <247058665+crazyrob425@users.noreply.github.com>
---
static/app/auth.js | 4 +-
static/app/provider-manager.js | 415 +++++++++++++++++++--------------
static/login.html | 9 +-
3 files changed, 245 insertions(+), 183 deletions(-)
diff --git a/static/app/auth.js b/static/app/auth.js
index a9fc412e7..59238451d 100644
--- a/static/app/auth.js
+++ b/static/app/auth.js
@@ -52,6 +52,8 @@ class AuthManager {
if (rememberMe) {
const expiryTime = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7天
localStorage.setItem(this.expiryKey, expiryTime.toString());
+ } else {
+ localStorage.removeItem(this.expiryKey);
}
}
@@ -366,4 +368,4 @@ export {
getAuthHeaders
};
-console.log('认证模块已加载');
\ No newline at end of file
+console.log('认证模块已加载');
diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js
index 9c208de53..99bd9050b 100644
--- a/static/app/provider-manager.js
+++ b/static/app/provider-manager.js
@@ -766,6 +766,27 @@ function generateAddGroupButton(providerType) {
* 处理生成授权链接
* @param {string} providerType - 提供商类型
*/
+function preOpenAuthPopup() {
+ const width = 600;
+ const height = 700;
+ const left = (window.screen.width - width) / 2 + 600;
+ const top = (window.screen.height - height) / 2;
+
+ try {
+ const popup = window.open(
+ '',
+ 'OAuthAuthWindow',
+ `width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes`
+ );
+ if (popup && popup.document) {
+ popup.document.title = 'OAuth';
+ }
+ return popup;
+ } catch (error) {
+ return null;
+ }
+}
+
async function handleGenerateAuthUrl(providerType) {
// 如果是 Kiro OAuth,先显示认证方式选择对话框
if (providerType === 'claude-kiro-oauth') {
@@ -785,7 +806,8 @@ async function handleGenerateAuthUrl(providerType) {
return;
}
- await executeGenerateAuthUrl(providerType, {});
+ const popupRef = preOpenAuthPopup();
+ await executeGenerateAuthUrl(providerType, { ui: { popupRef } });
}
/**
@@ -856,7 +878,8 @@ function showCodexAuthMethodSelector(providerType) {
if (method === 'batch-import') {
showCodexBatchImportModal(providerType);
} else {
- await executeGenerateAuthUrl(providerType, {});
+ const popupRef = preOpenAuthPopup();
+ await executeGenerateAuthUrl(providerType, { ui: { popupRef } });
}
});
});
@@ -1246,7 +1269,8 @@ function showKiroAuthMethodSelector(providerType) {
} else if (method === 'aws-import') {
showKiroAwsImportModal();
} else {
- await executeGenerateAuthUrl(providerType, { method });
+ const popupRef = preOpenAuthPopup();
+ await executeGenerateAuthUrl(providerType, { method, ui: { popupRef } });
}
});
});
@@ -1320,7 +1344,8 @@ function showGeminiAuthMethodSelector(providerType) {
if (method === 'batch-import') {
showGeminiBatchImportModal(providerType);
} else {
- await executeGenerateAuthUrl(providerType, {});
+ const popupRef = preOpenAuthPopup();
+ await executeGenerateAuthUrl(providerType, { ui: { popupRef } });
}
});
});
@@ -2744,6 +2769,7 @@ function showKiroAwsImportModal() {
*/
async function executeGenerateAuthUrl(providerType, extraOptions = {}) {
try {
+ const { ui = {}, ...requestOptions } = extraOptions;
showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info');
// 使用 fileUploadHandler 中的 getProviderKey 获取目录名称
@@ -2754,7 +2780,7 @@ async function executeGenerateAuthUrl(providerType, extraOptions = {}) {
{
saveToConfigs: true,
providerDir: providerDir,
- ...extraOptions
+ ...requestOptions
}
);
@@ -2778,7 +2804,7 @@ async function executeGenerateAuthUrl(providerType, extraOptions = {}) {
}
// 显示授权信息模态框
- showAuthModal(response.authUrl, response.authInfo);
+ showAuthModal(response.authUrl, response.authInfo, ui);
} else {
showToast(t('common.error'), t('modal.provider.auth.failed'), 'error');
}
@@ -2809,7 +2835,7 @@ function getAuthFilePath(provider) {
* @param {string} authUrl - 授权URL
* @param {Object} authInfo - 授权信息
*/
-function showAuthModal(authUrl, authInfo) {
+function showAuthModal(authUrl, authInfo, uiOptions = {}) {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.style.display = 'flex';
@@ -2961,6 +2987,206 @@ function showAuthModal(authUrl, authInfo) {
`;
document.body.appendChild(modal);
+
+ let authWindow = uiOptions.popupRef || null;
+ let pollTimer = null;
+ let popupInitialized = false;
+
+ const cleanupAuthListeners = () => {
+ if (pollTimer) {
+ clearInterval(pollTimer);
+ pollTimer = null;
+ }
+ window.removeEventListener('oauth_success_event', handleOAuthSuccess);
+ window.removeEventListener('message', handlePopupMessage);
+ popupInitialized = false;
+ };
+
+ // 监听 OAuth 成功事件,自动关闭窗口和模态框
+ const handleOAuthSuccess = () => {
+ if (authWindow && !authWindow.closed) {
+ authWindow.close();
+ }
+ modal.remove();
+ cleanupAuthListeners();
+
+ // 授权成功后刷新配置和提供商列表
+ loadProviders();
+ loadConfigList();
+ };
+
+ // 回调页主动 postMessage 时,优先使用父页面关闭子窗口
+ const handlePopupMessage = (event) => {
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+
+ const data = event.data;
+ if (!data || data.type !== 'oauth-popup-complete') {
+ return;
+ }
+
+ if (data.provider && data.provider !== authInfo.provider) {
+ return;
+ }
+
+ handleOAuthSuccess();
+ };
+
+ const ensureManualCallbackUi = () => {
+ const urlSection = modal.querySelector('.auth-url-section');
+ if (!urlSection || modal.querySelector('.manual-callback-section')) {
+ return;
+ }
+ const manualInputHtml = `
+
+
${t('oauth.manual.title')}
+
${t('oauth.manual.desc')}
+
+
+
+
+
+ `;
+ urlSection.insertAdjacentHTML('afterend', manualInputHtml);
+ };
+
+ const processCallback = (urlStr, isManualInput = false) => {
+ try {
+ // 尝试清理 URL(有些用户可能会复制多余的文字)
+ const cleanUrlStr = urlStr.trim().match(/https?:\/\/[^\s]+/)?.[0] || urlStr.trim();
+ const url = new URL(cleanUrlStr);
+
+ if (url.searchParams.has('code') || url.searchParams.has('token')) {
+ if (pollTimer) {
+ clearInterval(pollTimer);
+ pollTimer = null;
+ }
+ // 构造本地可处理的 URL,只修改 hostname,保持原始 URL 的端口号不变
+ const localUrl = new URL(url.href);
+ localUrl.hostname = window.location.hostname;
+ localUrl.protocol = window.location.protocol;
+
+ showToast(t('common.info'), t('oauth.processing'), 'info');
+
+ // 如果是手动输入,直接通过 fetch 请求处理,然后关闭子窗口
+ if (isManualInput) {
+ // 通过服务端API处理手动输入的回调URL
+ window.apiClient.post('/oauth/manual-callback', {
+ provider: authInfo.provider,
+ callbackUrl: url.href, //使用localhost访问
+ authMethod: authInfo.authMethod
+ })
+ .then(response => {
+ if (response.success) {
+ console.log('OAuth 回调处理成功');
+ handleOAuthSuccess();
+ showToast(t('common.success'), t('oauth.success.msg'), 'success');
+ } else {
+ console.error('OAuth 回调处理失败:', response.error);
+ showToast(t('common.error'), response.error || t('oauth.error.process'), 'error');
+ }
+ })
+ .catch(err => {
+ console.error('OAuth 回调请求失败:', err);
+ showToast(t('common.error'), t('oauth.error.process'), 'error');
+ });
+ } else {
+ // 自动监听模式:优先在子窗口中跳转(如果没关)
+ if (authWindow && !authWindow.closed) {
+ authWindow.location.href = localUrl.href;
+ } else {
+ // 备选方案:通过 fetch 请求
+ // 通过 fetch 请求本地服务器处理回调
+ fetch(localUrl.href)
+ .then(response => {
+ if (response.ok) {
+ console.log('OAuth 回调处理成功');
+ } else {
+ console.error('OAuth 回调处理失败:', response.status);
+ }
+ })
+ .catch(err => {
+ console.error('OAuth 回调请求失败:', err);
+ });
+ }
+ }
+
+ } else {
+ showToast(t('common.warning'), t('oauth.invalid.url'), 'warning');
+ }
+ } catch (err) {
+ console.error('处理回调失败:', err);
+ showToast(t('common.error'), t('oauth.error.format'), 'error');
+ }
+ };
+
+ const initializePopupHandlers = () => {
+ if (popupInitialized) return;
+ popupInitialized = true;
+ window.addEventListener('oauth_success_event', handleOAuthSuccess);
+ window.addEventListener('message', handlePopupMessage);
+ ensureManualCallbackUi();
+
+ const manualInput = modal.querySelector('.manual-callback-input');
+ const applyBtn = modal.querySelector('.apply-callback-btn');
+ if (applyBtn) {
+ applyBtn.addEventListener('click', () => {
+ processCallback(manualInput.value, true);
+ });
+ }
+
+ pollTimer = setInterval(() => {
+ try {
+ if (!authWindow || authWindow.closed) {
+ cleanupAuthListeners();
+ return;
+ }
+ // 如果能读到说明回到了同域
+ const currentUrl = authWindow.location.href;
+ if (currentUrl && (currentUrl.includes('code=') || currentUrl.includes('token='))) {
+ processCallback(currentUrl);
+ }
+ } catch (e) {
+ // 跨域受限是正常的
+ }
+ }, 1000);
+ };
+
+ const openAuthPopup = () => {
+ const width = 600;
+ const height = 700;
+ const left = (window.screen.width - width) / 2 + 600;
+ const top = (window.screen.height - height) / 2;
+
+ if (authWindow && !authWindow.closed) {
+ try {
+ authWindow.location.href = authUrl;
+ authWindow.focus();
+ } catch (err) {
+ // ignore
+ }
+ } else {
+ authWindow = window.open(
+ authUrl,
+ 'OAuthAuthWindow',
+ `width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes`
+ );
+ }
+
+ if (authWindow) {
+ showToast(t('common.info'), t('oauth.window.opened'), 'info');
+ initializePopupHandlers();
+ } else {
+ showToast(t('common.error'), t('oauth.window.blocked'), 'error');
+ }
+ };
+
+ if (authWindow && !authWindow.closed) {
+ openAuthPopup();
+ }
// 关闭按钮事件
const closeBtn = modal.querySelector('.modal-close');
@@ -3024,180 +3250,7 @@ function showAuthModal(authUrl, authInfo) {
// 在浏览器中打开按钮
const openBtn = modal.querySelector('.open-auth-btn');
openBtn.addEventListener('click', () => {
- // 使用子窗口打开,以便监听 URL 变化
- const width = 600;
- const height = 700;
- const left = (window.screen.width - width) / 2 + 600;
- const top = (window.screen.height - height) / 2;
-
- const authWindow = window.open(
- authUrl,
- 'OAuthAuthWindow',
- `width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes`
- );
-
- let pollTimer = null;
- const cleanupAuthListeners = () => {
- if (pollTimer) {
- clearInterval(pollTimer);
- pollTimer = null;
- }
- window.removeEventListener('oauth_success_event', handleOAuthSuccess);
- window.removeEventListener('message', handlePopupMessage);
- };
-
- // 监听 OAuth 成功事件,自动关闭窗口和模态框
- const handleOAuthSuccess = () => {
- if (authWindow && !authWindow.closed) {
- authWindow.close();
- }
- modal.remove();
- cleanupAuthListeners();
-
- // 授权成功后刷新配置和提供商列表
- loadProviders();
- loadConfigList();
- };
-
- // 回调页主动 postMessage 时,优先使用父页面关闭子窗口
- const handlePopupMessage = (event) => {
- if (event.origin !== window.location.origin) {
- return;
- }
-
- const data = event.data;
- if (!data || data.type !== 'oauth-popup-complete') {
- return;
- }
-
- if (data.provider && data.provider !== authInfo.provider) {
- return;
- }
-
- handleOAuthSuccess();
- };
-
- window.addEventListener('oauth_success_event', handleOAuthSuccess);
- window.addEventListener('message', handlePopupMessage);
-
- if (authWindow) {
- showToast(t('common.info'), t('oauth.window.opened'), 'info');
-
- // 添加手动输入回调 URL 的 UI
- const urlSection = modal.querySelector('.auth-url-section');
- if (urlSection && !modal.querySelector('.manual-callback-section')) {
- const manualInputHtml = `
-
-
${t('oauth.manual.title')}
-
${t('oauth.manual.desc')}
-
-
-
-
-
- `;
- urlSection.insertAdjacentHTML('afterend', manualInputHtml);
- }
-
- const manualInput = modal.querySelector('.manual-callback-input');
- const applyBtn = modal.querySelector('.apply-callback-btn');
-
- // 处理回调 URL 的核心逻辑
- const processCallback = (urlStr, isManualInput = false) => {
- try {
- // 尝试清理 URL(有些用户可能会复制多余的文字)
- const cleanUrlStr = urlStr.trim().match(/https?:\/\/[^\s]+/)?.[0] || urlStr.trim();
- const url = new URL(cleanUrlStr);
-
- if (url.searchParams.has('code') || url.searchParams.has('token')) {
- if (pollTimer) {
- clearInterval(pollTimer);
- pollTimer = null;
- }
- // 构造本地可处理的 URL,只修改 hostname,保持原始 URL 的端口号不变
- const localUrl = new URL(url.href);
- localUrl.hostname = window.location.hostname;
- localUrl.protocol = window.location.protocol;
-
- showToast(t('common.info'), t('oauth.processing'), 'info');
-
- // 如果是手动输入,直接通过 fetch 请求处理,然后关闭子窗口
- if (isManualInput) {
- // 通过服务端API处理手动输入的回调URL
- window.apiClient.post('/oauth/manual-callback', {
- provider: authInfo.provider,
- callbackUrl: url.href, //使用localhost访问
- authMethod: authInfo.authMethod
- })
- .then(response => {
- if (response.success) {
- console.log('OAuth 回调处理成功');
- handleOAuthSuccess();
- showToast(t('common.success'), t('oauth.success.msg'), 'success');
- } else {
- console.error('OAuth 回调处理失败:', response.error);
- showToast(t('common.error'), response.error || t('oauth.error.process'), 'error');
- }
- })
- .catch(err => {
- console.error('OAuth 回调请求失败:', err);
- showToast(t('common.error'), t('oauth.error.process'), 'error');
- });
- } else {
- // 自动监听模式:优先在子窗口中跳转(如果没关)
- if (authWindow && !authWindow.closed) {
- authWindow.location.href = localUrl.href;
- } else {
- // 备选方案:通过 fetch 请求
- // 通过 fetch 请求本地服务器处理回调
- fetch(localUrl.href)
- .then(response => {
- if (response.ok) {
- console.log('OAuth 回调处理成功');
- } else {
- console.error('OAuth 回调处理失败:', response.status);
- }
- })
- .catch(err => {
- console.error('OAuth 回调请求失败:', err);
- });
- }
- }
-
- } else {
- showToast(t('common.warning'), t('oauth.invalid.url'), 'warning');
- }
- } catch (err) {
- console.error('处理回调失败:', err);
- showToast(t('common.error'), t('oauth.error.format'), 'error');
- }
- };
-
- applyBtn.addEventListener('click', () => {
- processCallback(manualInput.value, true);
- });
-
- // 启动定时器轮询子窗口 URL
- pollTimer = setInterval(() => {
- try {
- if (authWindow.closed) {
- cleanupAuthListeners();
- return;
- }
- // 如果能读到说明回到了同域
- const currentUrl = authWindow.location.href;
- if (currentUrl && (currentUrl.includes('code=') || currentUrl.includes('token='))) {
- processCallback(currentUrl);
- }
- } catch (e) {
- // 跨域受限是正常的
- }
- }, 1000);
- } else {
- showToast(t('common.error'), t('oauth.window.blocked'), 'error');
- }
+ openAuthPopup();
});
}
diff --git a/static/login.html b/static/login.html
index 69ddff0a3..8d50aafe4 100644
--- a/static/login.html
+++ b/static/login.html
@@ -300,6 +300,7 @@ BlacklistedAPI
if (response.ok && data.success) {
// 登录成功,保存token
localStorage.setItem('authToken', data.token);
+ localStorage.removeItem('authTokenExpiry');
// 跳转到主页
window.location.href = '/';
@@ -331,6 +332,12 @@ BlacklistedAPI
function checkLoginStatus() {
const token = localStorage.getItem('authToken');
+ const expiry = localStorage.getItem('authTokenExpiry');
+ if (expiry && Date.now() > parseInt(expiry, 10)) {
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('authTokenExpiry');
+ return;
+ }
if (token) {
// Token存在,跳转到主页
@@ -347,4 +354,4 @@ BlacklistedAPI
passwordInput.focus();