Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Google OAuth 认证</title> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; | |
| max-width: 700px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .container { | |
| background: white; | |
| padding: 40px; | |
| border-radius: 15px; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.1); | |
| width: 100%; | |
| max-width: 600px; | |
| } | |
| h1 { | |
| color: #333; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| font-size: 28px; | |
| font-weight: 300; | |
| } | |
| .logo { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| .logo svg { | |
| width: 60px; | |
| height: 60px; | |
| } | |
| .form-group { | |
| margin-bottom: 25px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: #555; | |
| font-size: 14px; | |
| } | |
| input[type="text"], input[type="password"] { | |
| width: 100%; | |
| padding: 12px 16px; | |
| border: 2px solid #e1e5e9; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| box-sizing: border-box; | |
| transition: all 0.3s ease; | |
| } | |
| input[type="text"]:focus, input[type="password"]:focus { | |
| border-color: #4285f4; | |
| outline: none; | |
| box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1); | |
| } | |
| .btn { | |
| background: linear-gradient(135deg, #4285f4, #34a853); | |
| color: white; | |
| padding: 14px 30px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| width: 100%; | |
| margin-bottom: 15px; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(66, 133, 244, 0.3); | |
| } | |
| .btn:active { | |
| transform: translateY(0); | |
| } | |
| .btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .btn.secondary { | |
| background: linear-gradient(135deg, #34a853, #fbbc04); | |
| } | |
| .btn.download { | |
| background: linear-gradient(135deg, #ea4335, #ff6900); | |
| } | |
| .auth-url { | |
| background: #f8f9fa; | |
| border: 2px solid #e1e4e8; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 25px 0; | |
| word-break: break-all; | |
| border-left: 4px solid #4285f4; | |
| } | |
| .auth-url a { | |
| color: #4285f4; | |
| text-decoration: none; | |
| font-weight: 600; | |
| } | |
| .auth-url a:hover { | |
| text-decoration: underline; | |
| } | |
| .credentials { | |
| background: #f0f8ff; | |
| border: 2px solid #b0d4ff; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 25px 0; | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
| font-size: 12px; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| border-left: 4px solid #34a853; | |
| } | |
| .status { | |
| padding: 15px 20px; | |
| border-radius: 8px; | |
| margin: 20px 0; | |
| font-weight: 500; | |
| } | |
| .status.success { | |
| background: #d4edda; | |
| border: 2px solid #c3e6cb; | |
| color: #155724; | |
| border-left: 4px solid #28a745; | |
| } | |
| .status.error { | |
| background: #f8d7da; | |
| border: 2px solid #f5c6cb; | |
| color: #721c24; | |
| border-left: 4px solid #dc3545; | |
| } | |
| .status.info { | |
| background: #d1ecf1; | |
| border: 2px solid #bee5eb; | |
| color: #0c5460; | |
| border-left: 4px solid #17a2b8; | |
| } | |
| .status.warning { | |
| background: #fff3cd; | |
| border: 2px solid #ffeaa7; | |
| color: #856404; | |
| border-left: 4px solid #ffc107; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| .loading { | |
| text-align: center; | |
| color: #666; | |
| } | |
| .login-form { | |
| text-align: center; | |
| padding: 20px 0; | |
| } | |
| .steps { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 25px 0; | |
| border-left: 4px solid #ffc107; | |
| } | |
| .steps h4 { | |
| margin-top: 0; | |
| color: #333; | |
| font-size: 16px; | |
| } | |
| .steps ol { | |
| margin: 15px 0; | |
| padding-left: 20px; | |
| } | |
| .steps li { | |
| margin-bottom: 8px; | |
| color: #555; | |
| line-height: 1.5; | |
| } | |
| .api-warning { | |
| background: #fff3cd; | |
| border: 2px solid #ffeaa7; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 25px 0; | |
| border-left: 4px solid #ffc107; | |
| } | |
| .api-warning h4 { | |
| color: #856404; | |
| margin-top: 0; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .api-warning a { | |
| color: #4285f4; | |
| text-decoration: none; | |
| font-weight: 600; | |
| } | |
| .api-warning a:hover { | |
| text-decoration: underline; | |
| } | |
| .progress-bar { | |
| background: #e9ecef; | |
| border-radius: 10px; | |
| height: 8px; | |
| margin: 15px 0; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| background: linear-gradient(135deg, #28a745, #20c997); | |
| height: 100%; | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .footer { | |
| text-align: center; | |
| margin-top: 30px; | |
| padding-top: 20px; | |
| border-top: 1px solid #e1e5e9; | |
| color: #666; | |
| font-size: 14px; | |
| } | |
| @media (max-width: 768px) { | |
| body { | |
| padding: 10px; | |
| } | |
| .container { | |
| padding: 20px; | |
| } | |
| } | |
| /* 下载按钮动画 */ | |
| .download-animation { | |
| animation: downloadPulse 2s infinite; | |
| } | |
| @keyframes downloadPulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| 100% { transform: scale(1); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- 登录界面 --> | |
| <div id="loginSection"> | |
| <div class="logo"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/> | |
| <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/> | |
| <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/> | |
| <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/> | |
| </svg> | |
| </div> | |
| <h1>Google OAuth 认证</h1> | |
| <div class="login-form"> | |
| <div class="form-group"> | |
| <input type="password" id="password" placeholder="请输入访问密码" onkeypress="handlePasswordEnter(event)" /> | |
| </div> | |
| <button class="btn" onclick="login()">登录</button> | |
| </div> | |
| </div> | |
| <!-- 主界面 --> | |
| <div id="mainSection" class="hidden"> | |
| <h1>Google OAuth 认证</h1> | |
| <!-- API 自动启用说明 --> | |
| <div class="status info"> | |
| <h4>✨ 自动化优化</h4> | |
| <p style="margin: 15px 0;">系统现在会在认证成功后自动为您的项目启用以下必需的API服务:</p> | |
| <ul style="margin: 15px 0; padding-left: 20px;"> | |
| <li><strong>Gemini Cloud Assist API</strong></li> | |
| <li><strong>Gemini for Google Cloud API</strong></li> | |
| </ul> | |
| <p style="margin: 15px 0; color: #0c5460;"><strong>说明:</strong>无需手动启用API,系统会自动处理这些配置步骤。</p> | |
| </div> | |
| <!-- 折叠式 Project ID 输入框 --> | |
| <div class="form-group"> | |
| <div style="cursor: pointer; user-select: none; padding: 12px; border: 2px solid #e1e5e9; border-radius: 8px; background: #f8f9fa; display: flex; justify-content: space-between; align-items: center;" onclick="toggleProjectIdSection()"> | |
| <span style="font-weight: 600; color: #555;">📁 高级选项:Google Cloud Project ID (不用管,直接点击获取链接即可)</span> | |
| <span id="projectIdToggleIcon" style="font-size: 14px; color: #666; transition: transform 0.3s ease;">▶</span> | |
| </div> | |
| <div id="projectIdSection" style="display: none; margin-top: 15px; padding: 15px; border: 2px solid #e1e5e9; border-top: none; border-radius: 0 0 8px 8px; background: #ffffff;"> | |
| <label for="projectId" style="display: block; margin-bottom: 8px; font-weight: 600; color: #555; font-size: 14px;">Google Cloud Project ID (可选):</label> | |
| <input type="text" id="projectId" style="width: 100%; padding: 12px 16px; border: 2px solid #e1e5e9; border-radius: 8px; font-size: 16px; box-sizing: border-box; transition: all 0.3s ease;" placeholder="留空将尝试自动检测,或手动输入项目ID" /> | |
| <small style="color: #666; font-size: 12px; margin-top: 5px; display: block;"> | |
| 💡 提示:如果你不懂这是什么,可以留空此字段让系统自动检测项目ID | |
| </small> | |
| </div> | |
| </div> | |
| <button class="btn" id="getAuthBtn" onclick="startAuth()">获取认证链接</button> | |
| <div id="authUrlSection" class="hidden"> | |
| <h3>步骤一:认证链接</h3> | |
| <div class="auth-url"> | |
| <a id="authUrl" href="#" target="_blank">点击此链接进行 OAuth 认证</a> | |
| </div> | |
| <div class="steps"> | |
| <h4>认证步骤:</h4> | |
| <ol> | |
| <li>点击上方认证链接,在新窗口中完成 Google 登录</li> | |
| <li>授权应用访问您的 Google Cloud 项目</li> | |
| <li>看到 "OAuth authentication successful!" 页面后,关闭该窗口</li> | |
| <li>返回本页面,点击下方"获取认证文件"按钮</li> | |
| </ol> | |
| <div class="status warning" style="margin-top: 20px;"> | |
| <h4>⚠️ 注意</h4> | |
| <p>当回源时,网页访问会显示报错。这时请手动将浏览器地址栏的 <code>localhost</code> 修改为 <code>gcli-auth.sukaka.top</code>,然后重新访问即可。</p> | |
| </div> | |
| </div> | |
| <!-- 快捷回调URL输入选项 --> | |
| <div style="margin: 20px 0; padding: 15px; border: 2px solid #e8f4fd; border-radius: 8px; background: #f8fcff;"> | |
| <div style="cursor: pointer; user-select: none; display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;" onclick="toggleCallbackUrlSection()"> | |
| <span style="font-weight: 600; color: #0066cc; font-size: 16px;">🚀 无法回源?试试快捷方式</span> | |
| <span id="callbackUrlToggleIcon" style="font-size: 14px; color: #666; transition: transform 0.3s ease;">▼</span> | |
| </div> | |
| <div id="callbackUrlSection" style="display: none;"> | |
| <div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 12px; margin-bottom: 12px;"> | |
| <div style="color: #856404; font-size: 14px; font-weight: bold; margin-bottom: 6px;">📚 适用场景:</div> | |
| <ul style="color: #856404; font-size: 13px; margin: 0; padding-left: 18px; line-height: 1.5;"> | |
| <li>云服务器、VPS等非本地环境</li> | |
| <li>防火墙阻止了回调端口访问</li> | |
| <li>网络环境无法正常回源</li> | |
| <li>Docker容器端口映射问题</li> | |
| </ul> | |
| </div> | |
| <div style="color: #666; font-size: 13px; margin-bottom: 12px; line-height: 1.6;"> | |
| <strong style="color: #0066cc;">🔍 什么是回调URL?</strong><br> | |
| 完成Google OAuth授权后,浏览器地址栏显示的完整URL,通常看起来像这样:<br> | |
| <code style="background: #f1f3f4; padding: 2px 6px; border-radius: 3px; font-size: 12px; word-break: break-all; display: block; margin-top: 4px;"> | |
| http://localhost:8080/?state=abc123...&code=4/0AVMBsJ...&scope=email%20profile... | |
| </code> | |
| </div> | |
| <div style="background: #e7f3ff; border: 1px solid #b3d9ff; border-radius: 6px; padding: 10px; margin-bottom: 12px;"> | |
| <div style="color: #0066cc; font-size: 13px; font-weight: bold; margin-bottom: 4px;">📋 使用步骤:</div> | |
| <ol style="color: #0066cc; font-size: 12px; margin: 0; padding-left: 18px; line-height: 1.4;"> | |
| <li>点击上方认证链接,完成Google授权</li> | |
| <li>授权成功后,复制浏览器地址栏的<strong>完整URL</strong></li> | |
| <li>粘贴到下方输入框,点击获取凭证即可</li> | |
| </ol> | |
| </div> | |
| <div style="margin-bottom: 12px;"> | |
| <input type="url" id="callbackUrlInput" placeholder="粘贴完整的回调URL,例如:http://localhost:8080/?state=xxx&code=xxx&scope=xxx..." | |
| style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 4px; font-size: 13px; box-sizing: border-box;"> | |
| </div> | |
| <button class="btn" style="background: #28a745; border-color: #28a745;" onclick="processCallbackUrl()"> | |
| 从回调URL获取凭证 | |
| </button> | |
| </div> | |
| </div> | |
| <button class="btn secondary" id="getCredsBtn" onclick="getCredentials()">获取认证文件</button> | |
| </div> | |
| <div id="credentialsSection" class="hidden"> | |
| <h3>步骤二:认证成功!</h3> | |
| <div class="status success"> | |
| <strong>✅ OAuth 认证成功完成!</strong><br> | |
| 认证文件已生成,您可以下载保存或复制使用。 | |
| </div> | |
| <div class="credentials" id="credentialsContent"></div> | |
| <button class="btn download download-animation" id="downloadBtn" onclick="downloadCredentials()"> | |
| 📥 下载认证文件 | |
| </button> | |
| <button class="btn" onclick="resetForm()">认证其他项目</button> | |
| </div> | |
| <div id="statusSection"></div> | |
| <div class="footer"> | |
| <p>🔒 您的认证信息将安全保存,仅用于访问指定的 Google Cloud 项目</p> | |
| <!-- 项目信息 --> | |
| <div style="background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px; margin-top: 20px; text-align: center; border-left: 4px solid #17a2b8;"> | |
| <p style="margin: 6px 0; font-size: 15px; color: #495057;">GitHub: <a href="https://github.com/su-kaka/gcli2api" target="_blank" style="color: #17a2b8; text-decoration: none; font-weight: 500;">https://github.com/su-kaka/gcli2api</a></p> | |
| <p style="margin: 6px 0; font-size: 15px; color: #dc3545; font-weight: 500;">⚠️ 禁止商业用途和倒卖 - 仅供学习使用 ⚠️</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentProjectId = ''; | |
| let authInProgress = false; | |
| let authToken = ''; | |
| let currentCredentials = null; | |
| function showStatus(message, type = 'info') { | |
| const statusSection = document.getElementById('statusSection'); | |
| if (statusSection) { | |
| statusSection.innerHTML = `<div class="status ${type}">${message}</div>`; | |
| // 自动滚动到状态消息 | |
| statusSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| } | |
| // 登录相关函数 | |
| async function login() { | |
| const password = document.getElementById('password').value; | |
| if (!password) { | |
| showStatus('请输入密码', 'error'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/auth/login', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ password: password }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| authToken = data.token; | |
| document.getElementById('loginSection').classList.add('hidden'); | |
| document.getElementById('mainSection').classList.remove('hidden'); | |
| showStatus('登录成功', 'success'); | |
| } else { | |
| showStatus(`登录失败: ${data.detail || data.error || '密码错误'}`, 'error'); | |
| } | |
| } catch (error) { | |
| showStatus(`网络错误: ${error.message}`, 'error'); | |
| } | |
| } | |
| function handlePasswordEnter(event) { | |
| if (event.key === 'Enter') { | |
| login(); | |
| } | |
| } | |
| // 获取认证头 | |
| function getAuthHeaders() { | |
| return { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${authToken}` | |
| }; | |
| } | |
| async function startAuth() { | |
| const projectId = document.getElementById('projectId').value.trim(); | |
| // 项目ID现在是可选的 | |
| currentProjectId = projectId || null; | |
| const btn = document.getElementById('getAuthBtn'); | |
| btn.disabled = true; | |
| btn.textContent = '正在生成认证链接...'; | |
| try { | |
| const requestBody = {}; | |
| if (projectId) { | |
| requestBody.project_id = projectId; | |
| showStatus('使用指定的项目ID生成认证链接...', 'info'); | |
| } else { | |
| showStatus('将尝试自动检测项目ID,正在生成认证链接...', 'info'); | |
| } | |
| const response = await fetch('/auth/start', { | |
| method: 'POST', | |
| headers: getAuthHeaders(), | |
| body: JSON.stringify(requestBody) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| document.getElementById('authUrl').href = data.auth_url; | |
| document.getElementById('authUrl').textContent = data.auth_url; | |
| document.getElementById('authUrlSection').classList.remove('hidden'); | |
| if (data.auto_project_detection) { | |
| showStatus('认证链接已生成(将在认证完成后自动检测项目ID),请点击链接完成 OAuth 授权', 'info'); | |
| } else { | |
| showStatus(`认证链接已生成(项目ID: ${data.detected_project_id}),请点击链接完成 OAuth 授权`, 'info'); | |
| } | |
| authInProgress = true; | |
| } else { | |
| showStatus(`生成认证链接失败: ${data.error || '未知错误'}`, 'error'); | |
| } | |
| } catch (error) { | |
| showStatus(`网络错误: ${error.message}`, 'error'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = '获取认证链接'; | |
| } | |
| } | |
| async function getCredentials() { | |
| if (!authInProgress) { | |
| showStatus('请先获取认证链接并完成 OAuth 授权', 'error'); | |
| return; | |
| } | |
| const btn = document.getElementById('getCredsBtn'); | |
| btn.disabled = true; | |
| btn.textContent = '等待 OAuth 回调...'; | |
| try { | |
| showStatus('正在等待 OAuth 回调,请确保已完成浏览器中的授权...', 'info'); | |
| const requestBody = {}; | |
| if (currentProjectId) { | |
| requestBody.project_id = currentProjectId; | |
| } | |
| const response = await fetch('/auth/callback', { | |
| method: 'POST', | |
| headers: getAuthHeaders(), | |
| body: JSON.stringify(requestBody) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| currentCredentials = data.credentials; | |
| document.getElementById('credentialsContent').textContent = JSON.stringify(data.credentials, null, 2); | |
| document.getElementById('credentialsSection').classList.remove('hidden'); | |
| if (data.auto_detected_project) { | |
| showStatus(`认证成功!项目ID已自动检测为: ${data.credentials.project_id},文件已保存到服务器: ${data.file_path}`, 'success'); | |
| } else { | |
| showStatus(`认证成功!文件已保存到服务器: ${data.file_path}`, 'success'); | |
| } | |
| authInProgress = false; | |
| // 隐藏前面的步骤,突出显示成功结果 | |
| document.getElementById('authUrlSection').style.opacity = '0.6'; | |
| } else { | |
| // 检查是否需要项目选择 | |
| if (data.requires_project_selection && data.available_projects) { | |
| let projectOptions = "请选择一个项目:\n\n"; | |
| data.available_projects.forEach((project, index) => { | |
| projectOptions += `${index + 1}. ${project.name} (${project.projectId})\n`; | |
| }); | |
| projectOptions += `\n请输入序号 (1-${data.available_projects.length}):`; | |
| const selection = prompt(projectOptions); | |
| const projectIndex = parseInt(selection) - 1; | |
| if (projectIndex >= 0 && projectIndex < data.available_projects.length) { | |
| const selectedProject = data.available_projects[projectIndex]; | |
| currentProjectId = selectedProject.projectId; | |
| btn.textContent = '重新尝试获取认证文件'; | |
| showStatus(`使用选择的项目 ${selectedProject.name} (${selectedProject.projectId}) 重新尝试...`, 'info'); | |
| setTimeout(() => getCredentials(), 1000); | |
| return; | |
| } else { | |
| showStatus('无效的选择,请重新开始认证', 'error'); | |
| } | |
| } | |
| // 检查是否需要手动输入项目ID | |
| else if (data.requires_manual_project_id) { | |
| const userProjectId = prompt('无法自动检测项目ID,请手动输入您的Google Cloud项目ID:'); | |
| if (userProjectId && userProjectId.trim()) { | |
| // 重新尝试,使用用户输入的项目ID | |
| currentProjectId = userProjectId.trim(); | |
| btn.textContent = '重新尝试获取认证文件'; | |
| showStatus('使用手动输入的项目ID重新尝试...', 'info'); | |
| setTimeout(() => getCredentials(), 1000); | |
| return; | |
| } else { | |
| showStatus('需要项目ID才能完成认证,请重新开始并输入正确的项目ID', 'error'); | |
| } | |
| } else { | |
| showStatus(`认证失败: ${data.error || '获取认证文件失败'}`, 'error'); | |
| if (data.error && data.error.includes('未接收到授权回调')) { | |
| showStatus('提示:请确保已完成浏览器中的 OAuth 认证,并看到了"OAuth authentication successful"页面', 'warning'); | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| showStatus(`网络错误: ${error.message}`, 'error'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = '获取认证文件'; | |
| } | |
| } | |
| function downloadCredentials() { | |
| if (!currentCredentials) { | |
| showStatus('没有可下载的认证文件', 'error'); | |
| return; | |
| } | |
| const filename = `${currentProjectId}-credentials.json`; | |
| const content = JSON.stringify(currentCredentials, null, 2); | |
| const blob = new Blob([content], { type: 'application/json' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| showStatus(`认证文件已下载: ${filename}`, 'success'); | |
| } | |
| function resetForm() { | |
| // 重置表单 | |
| document.getElementById('projectId').value = ''; | |
| document.getElementById('authUrlSection').classList.add('hidden'); | |
| document.getElementById('credentialsSection').classList.add('hidden'); | |
| document.getElementById('authUrlSection').style.opacity = '1'; | |
| // 重置状态 | |
| currentProjectId = ''; | |
| authInProgress = false; | |
| currentCredentials = null; | |
| showStatus('请输入新的 Project ID 开始认证', 'info'); | |
| } | |
| // Project ID 折叠切换函数 | |
| function toggleProjectIdSection() { | |
| const section = document.getElementById('projectIdSection'); | |
| const icon = document.getElementById('projectIdToggleIcon'); | |
| if (section.style.display === 'none') { | |
| section.style.display = 'block'; | |
| icon.style.transform = 'rotate(90deg)'; | |
| icon.textContent = '▼'; | |
| } else { | |
| section.style.display = 'none'; | |
| icon.style.transform = 'rotate(0deg)'; | |
| icon.textContent = '▶'; | |
| } | |
| } | |
| // 回调URL输入区域折叠切换函数 | |
| function toggleCallbackUrlSection() { | |
| const section = document.getElementById('callbackUrlSection'); | |
| const icon = document.getElementById('callbackUrlToggleIcon'); | |
| if (section.style.display === 'none') { | |
| section.style.display = 'block'; | |
| icon.style.transform = 'rotate(180deg)'; | |
| icon.textContent = '▲'; | |
| } else { | |
| section.style.display = 'none'; | |
| icon.style.transform = 'rotate(0deg)'; | |
| icon.textContent = '▼'; | |
| } | |
| } | |
| // 处理回调URL的函数 | |
| async function processCallbackUrl() { | |
| const callbackUrlInput = document.getElementById('callbackUrlInput'); | |
| const callbackUrl = callbackUrlInput.value.trim(); | |
| if (!callbackUrl) { | |
| showStatus('请输入回调URL', 'error'); | |
| return; | |
| } | |
| // 简单验证URL格式 | |
| if (!callbackUrl.startsWith('http://') && !callbackUrl.startsWith('https://')) { | |
| showStatus('请输入有效的URL(以http://或https://开头)', 'error'); | |
| return; | |
| } | |
| // 检查是否包含必要参数 | |
| if (!callbackUrl.includes('code=') || !callbackUrl.includes('state=')) { | |
| showStatus('❌ 这不是有效的回调URL!请确保:\n1. 已完成Google OAuth授权\n2. 复制的是浏览器地址栏的完整URL\n3. URL包含code和state参数', 'error'); | |
| return; | |
| } | |
| showStatus('正在从回调URL获取凭证...', 'info'); | |
| try { | |
| // 获取当前项目ID设置(如果有的话) | |
| const projectIdInput = document.getElementById('projectId'); | |
| const projectId = projectIdInput ? projectIdInput.value.trim() : null; | |
| const response = await fetch('/auth/callback-url', { | |
| method: 'POST', | |
| headers: getAuthHeaders(), | |
| body: JSON.stringify({ | |
| callback_url: callbackUrl, | |
| project_id: projectId || null | |
| }) | |
| }); | |
| const result = await response.json(); | |
| if (result.credentials) { | |
| // 显示成功信息 | |
| showStatus(result.message || '从回调URL获取凭证成功!', 'success'); | |
| // 隐藏认证URL区域,显示凭证内容 | |
| document.getElementById('authUrlSection').classList.add('hidden'); | |
| document.getElementById('credentialsSection').classList.remove('hidden'); | |
| // 显示凭证内容 | |
| document.getElementById('credentialsContent').innerHTML = | |
| '<pre>' + JSON.stringify(result.credentials, null, 2) + '</pre>'; | |
| // 设置全局变量供下载使用 | |
| window.currentCredentials = result.credentials; | |
| // 清空输入框 | |
| callbackUrlInput.value = ''; | |
| } else if (result.requires_manual_project_id) { | |
| showStatus('需要手动指定项目ID,请在高级选项中填入Google Cloud项目ID后重试', 'error'); | |
| } else if (result.requires_project_selection) { | |
| let projectOptions = '\n\n可用项目:\n'; | |
| result.available_projects.forEach(project => { | |
| projectOptions += `• ${project.name} (ID: ${project.projectId})\n`; | |
| }); | |
| showStatus('检测到多个项目,请在高级选项中指定项目ID:' + projectOptions, 'error'); | |
| } else { | |
| showStatus(result.error || '从回调URL获取凭证失败', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('从回调URL获取凭证时出错:', error); | |
| showStatus(`从回调URL获取凭证失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 页面加载时的初始化 | |
| window.onload = function() { | |
| showStatus('请输入密码登录以开始 OAuth 认证', 'info'); | |
| // 自动聚焦到密码输入框 | |
| document.getElementById('password').focus(); | |
| }; | |
| </script> | |
| </body> | |
| </html> |