|
|
@@ -400,9 +400,41 @@
|
|
|
<p class="text-sm text-gray-500">通用 MCP 服务器 Web 界面</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div id="status-indicator" class="flex items-center space-x-2">
|
|
|
- <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
|
|
- <span class="text-sm text-gray-600">已连接</span>
|
|
|
+ <div class="flex items-center space-x-4">
|
|
|
+ <!-- 用户菜单 (登录后显示) -->
|
|
|
+ <div id="user-menu" class="hidden relative">
|
|
|
+ <button id="user-menu-btn" class="flex items-center space-x-2 px-3 py-1.5 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors">
|
|
|
+ <div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-full flex items-center justify-center">
|
|
|
+ <span id="user-avatar" class="text-white text-sm font-bold">U</span>
|
|
|
+ </div>
|
|
|
+ <span id="user-name" class="text-sm font-medium text-gray-700">用户</span>
|
|
|
+ <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <!-- 下拉菜单 -->
|
|
|
+ <div id="user-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
|
|
+ <div class="px-4 py-2 border-b border-gray-100">
|
|
|
+ <p class="text-xs text-gray-500">已登录</p>
|
|
|
+ <p id="dropdown-username" class="text-sm font-medium text-gray-800">用户名</p>
|
|
|
+ </div>
|
|
|
+ <button id="logout-btn" class="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 transition-colors flex items-center space-x-2">
|
|
|
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
|
|
+ </svg>
|
|
|
+ <span>退出登录</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 登录按钮 (未登录时显示) -->
|
|
|
+ <button id="header-login-btn" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
|
|
|
+ 登录
|
|
|
+ </button>
|
|
|
+ <!-- 状态指示器 -->
|
|
|
+ <div id="status-indicator" class="flex items-center space-x-2">
|
|
|
+ <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
|
|
+ <span class="text-sm text-gray-600">已连接</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</header>
|
|
|
@@ -457,14 +489,27 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 登录对话框 -->
|
|
|
+ <!-- 登录/注册对话框 -->
|
|
|
<div id="login-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
|
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
<h3 class="text-xl font-bold text-gray-800" id="login-title">登录 Novel Platform</h3>
|
|
|
<button id="login-close-x" class="text-gray-500 hover:text-gray-700 text-2xl">×</button>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- Tab 切换 -->
|
|
|
+ <div class="flex border-b border-gray-200 mb-4">
|
|
|
+ <button id="tab-login" class="flex-1 py-2 text-center font-medium text-purple-600 border-b-2 border-purple-600 transition-colors">
|
|
|
+ 登录
|
|
|
+ </button>
|
|
|
+ <button id="tab-register" class="flex-1 py-2 text-center font-medium text-gray-500 hover:text-gray-700 transition-colors">
|
|
|
+ 注册
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
<p id="login-description" class="text-sm text-gray-600 mb-4">请输入您的账号密码以访问需要认证的 MCP 工具</p>
|
|
|
+
|
|
|
+ <!-- 登录表单 -->
|
|
|
<form id="login-form">
|
|
|
<div class="mb-3">
|
|
|
<label for="login-username" class="block text-sm font-medium text-gray-700 mb-1">用户名/邮箱</label>
|
|
|
@@ -480,6 +525,32 @@
|
|
|
<button type="submit" id="login-submit" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">登录</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
+
|
|
|
+ <!-- 注册表单 (默认隐藏) -->
|
|
|
+ <form id="register-form" class="hidden">
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="register-email" class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
|
|
+ <input type="email" id="register-email" placeholder="请输入邮箱" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
|
+ </div>
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="register-username" class="block text-sm font-medium text-gray-700 mb-1">用户名</label>
|
|
|
+ <input type="text" id="register-username" placeholder="请输入用户名" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
|
+ </div>
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="register-password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
|
|
+ <input type="password" id="register-password" placeholder="请输入密码" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
|
+ </div>
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="register-confirm-password" class="block text-sm font-medium text-gray-700 mb-1">确认密码</label>
|
|
|
+ <input type="password" id="register-confirm-password" placeholder="请再次输入密码" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
|
+ </div>
|
|
|
+ <p id="register-error" class="text-red-500 text-sm mb-3 hidden"></p>
|
|
|
+ <p id="register-success" class="text-green-500 text-sm mb-3 hidden"></p>
|
|
|
+ <div class="flex justify-end gap-2">
|
|
|
+ <button type="button" id="register-cancel" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">取消</button>
|
|
|
+ <button type="submit" id="register-submit" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">注册</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -561,15 +632,48 @@
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // ========== 登录相关 ==========
|
|
|
+ // ========== 登录/注册相关 ==========
|
|
|
let currentLoginMcpType = null;
|
|
|
+ let currentAuthTab = 'login'; // 'login' or 'register'
|
|
|
+ let currentUser = null; // 当前登录用户
|
|
|
+
|
|
|
const loginModal = document.getElementById('login-modal');
|
|
|
const loginForm = document.getElementById('login-form');
|
|
|
+ const registerForm = document.getElementById('register-form');
|
|
|
const loginUsername = document.getElementById('login-username');
|
|
|
const loginPassword = document.getElementById('login-password');
|
|
|
const loginError = document.getElementById('login-error');
|
|
|
const loginTitle = document.getElementById('login-title');
|
|
|
const loginSubmit = document.getElementById('login-submit');
|
|
|
+ const loginDescription = document.getElementById('login-description');
|
|
|
+
|
|
|
+ // Tab 切换
|
|
|
+ const tabLogin = document.getElementById('tab-login');
|
|
|
+ const tabRegister = document.getElementById('tab-register');
|
|
|
+
|
|
|
+ function switchTab(tab) {
|
|
|
+ currentAuthTab = tab;
|
|
|
+ if (tab === 'login') {
|
|
|
+ tabLogin.classList.add('text-purple-600', 'border-b-2', 'border-purple-600');
|
|
|
+ tabLogin.classList.remove('text-gray-500');
|
|
|
+ tabRegister.classList.remove('text-purple-600', 'border-b-2', 'border-purple-600');
|
|
|
+ tabRegister.classList.add('text-gray-500');
|
|
|
+ loginForm.classList.remove('hidden');
|
|
|
+ registerForm.classList.add('hidden');
|
|
|
+ loginDescription.textContent = '请输入您的账号密码以访问需要认证的 MCP 工具';
|
|
|
+ } else {
|
|
|
+ tabRegister.classList.add('text-purple-600', 'border-b-2', 'border-purple-600');
|
|
|
+ tabRegister.classList.remove('text-gray-500');
|
|
|
+ tabLogin.classList.remove('text-purple-600', 'border-b-2', 'border-purple-600');
|
|
|
+ tabLogin.classList.add('text-gray-500');
|
|
|
+ registerForm.classList.remove('hidden');
|
|
|
+ loginForm.classList.add('hidden');
|
|
|
+ loginDescription.textContent = '创建新账号以访问需要认证的 MCP 工具';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ tabLogin.addEventListener('click', () => switchTab('login'));
|
|
|
+ tabRegister.addEventListener('click', () => switchTab('register'));
|
|
|
|
|
|
// 显示登录对话框
|
|
|
function showLoginModal(mcpType) {
|
|
|
@@ -577,13 +681,23 @@
|
|
|
const config = MCP_SERVERS[mcpType];
|
|
|
if (!config) return;
|
|
|
|
|
|
- loginTitle.textContent = `登录 ${config.name}`;
|
|
|
+ loginTitle.textContent = config.name;
|
|
|
+ // 重置为登录 Tab
|
|
|
+ switchTab('login');
|
|
|
loginUsername.value = '';
|
|
|
loginPassword.value = '';
|
|
|
loginError.classList.add('hidden');
|
|
|
loginSubmit.disabled = false;
|
|
|
loginModal.classList.remove('hidden');
|
|
|
loginUsername.focus();
|
|
|
+
|
|
|
+ // 清空注册表单
|
|
|
+ document.getElementById('register-email').value = '';
|
|
|
+ document.getElementById('register-username').value = '';
|
|
|
+ document.getElementById('register-password').value = '';
|
|
|
+ document.getElementById('register-confirm-password').value = '';
|
|
|
+ document.getElementById('register-error').classList.add('hidden');
|
|
|
+ document.getElementById('register-success').classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
// 隐藏登录对话框
|
|
|
@@ -617,8 +731,11 @@
|
|
|
|
|
|
if (response.ok && data.success) {
|
|
|
const token = data.token;
|
|
|
- TokenManager.saveToken(mcpType, token, data.username || username);
|
|
|
- return { success: true, token, username: data.username || username };
|
|
|
+ const loggedUsername = data.username || username;
|
|
|
+ TokenManager.saveToken(mcpType, token, loggedUsername);
|
|
|
+ currentUser = loggedUsername;
|
|
|
+ updateUserMenu();
|
|
|
+ return { success: true, token, username: loggedUsername };
|
|
|
} else {
|
|
|
return { success: false, error: data.detail || data.error || '登录失败' };
|
|
|
}
|
|
|
@@ -627,6 +744,31 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 执行注册
|
|
|
+ async function performRegister(email, username, password) {
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/auth/register', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ email: email,
|
|
|
+ username: username,
|
|
|
+ password: password
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ return { success: true, message: data.message || '注册成功' };
|
|
|
+ } else {
|
|
|
+ return { success: false, error: data.detail || data.error || '注册失败' };
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ return { success: false, error: e.message };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 登录表单提交处理
|
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
|
e.preventDefault();
|
|
|
@@ -656,8 +798,71 @@
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+ // 注册表单提交处理
|
|
|
+ registerForm.addEventListener('submit', async (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ const email = document.getElementById('register-email').value.trim();
|
|
|
+ const username = document.getElementById('register-username').value.trim();
|
|
|
+ const password = document.getElementById('register-password').value;
|
|
|
+ const confirmPassword = document.getElementById('register-confirm-password').value;
|
|
|
+ const registerError = document.getElementById('register-error');
|
|
|
+ const registerSuccess = document.getElementById('register-success');
|
|
|
+ const registerSubmit = document.getElementById('register-submit');
|
|
|
+
|
|
|
+ // 验证输入
|
|
|
+ if (!email || !username || !password || !confirmPassword) {
|
|
|
+ registerError.textContent = '请填写所有字段';
|
|
|
+ registerError.classList.remove('hidden');
|
|
|
+ registerSuccess.classList.add('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证邮箱格式
|
|
|
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
+ if (!emailRegex.test(email)) {
|
|
|
+ registerError.textContent = '请输入有效的邮箱地址';
|
|
|
+ registerError.classList.remove('hidden');
|
|
|
+ registerSuccess.classList.add('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证密码匹配
|
|
|
+ if (password !== confirmPassword) {
|
|
|
+ registerError.textContent = '两次输入的密码不一致';
|
|
|
+ registerError.classList.remove('hidden');
|
|
|
+ registerSuccess.classList.add('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证密码长度
|
|
|
+ if (password.length < 6) {
|
|
|
+ registerError.textContent = '密码长度至少为 6 位';
|
|
|
+ registerError.classList.remove('hidden');
|
|
|
+ registerSuccess.classList.add('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ registerSubmit.disabled = true;
|
|
|
+ registerError.classList.add('hidden');
|
|
|
+ registerSuccess.classList.add('hidden');
|
|
|
+
|
|
|
+ const result = await performRegister(email, username, password);
|
|
|
+
|
|
|
+ if (result.success) {
|
|
|
+ registerSuccess.textContent = result.message + ',请切换到登录页面登录';
|
|
|
+ registerSuccess.classList.remove('hidden');
|
|
|
+ registerSubmit.disabled = false;
|
|
|
+ } else {
|
|
|
+ registerError.textContent = result.error;
|
|
|
+ registerError.classList.remove('hidden');
|
|
|
+ registerSubmit.disabled = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
// 登录对话框事件绑定
|
|
|
document.getElementById('login-cancel').addEventListener('click', hideLoginModal);
|
|
|
+ document.getElementById('register-cancel').addEventListener('click', hideLoginModal);
|
|
|
document.getElementById('login-close-x').addEventListener('click', hideLoginModal);
|
|
|
|
|
|
// 点击背景关闭
|
|
|
@@ -667,6 +872,79 @@
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+ // ========== 用户菜单相关 ==========
|
|
|
+ const userMenu = document.getElementById('user-menu');
|
|
|
+ const userMenuBtn = document.getElementById('user-menu-btn');
|
|
|
+ const userDropdown = document.getElementById('user-dropdown');
|
|
|
+ const headerLoginBtn = document.getElementById('header-login-btn');
|
|
|
+ const logoutBtn = document.getElementById('logout-btn');
|
|
|
+
|
|
|
+ // 更新用户菜单显示状态
|
|
|
+ function updateUserMenu() {
|
|
|
+ // 检查是否有任何 MCP 服务器已登录
|
|
|
+ let isLoggedIn = false;
|
|
|
+ let loggedInUsername = null;
|
|
|
+
|
|
|
+ for (const [mcpType, config] of Object.entries(MCP_SERVERS)) {
|
|
|
+ if (config.auth_type === 'jwt' && TokenManager.isLoggedIn(mcpType)) {
|
|
|
+ isLoggedIn = true;
|
|
|
+ loggedInUsername = TokenManager.getUsername(mcpType);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isLoggedIn) {
|
|
|
+ userMenu.classList.remove('hidden');
|
|
|
+ headerLoginBtn.classList.add('hidden');
|
|
|
+
|
|
|
+ // 更新用户信息显示
|
|
|
+ if (loggedInUsername) {
|
|
|
+ document.getElementById('user-name').textContent = loggedInUsername;
|
|
|
+ document.getElementById('dropdown-username').textContent = loggedInUsername;
|
|
|
+ document.getElementById('user-avatar').textContent = loggedInUsername.charAt(0).toUpperCase();
|
|
|
+ currentUser = loggedInUsername;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ userMenu.classList.add('hidden');
|
|
|
+ headerLoginBtn.classList.remove('hidden');
|
|
|
+ currentUser = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 用户菜单按钮点击 - 切换下拉菜单
|
|
|
+ userMenuBtn.addEventListener('click', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ userDropdown.classList.toggle('hidden');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 点击其他地方关闭下拉菜单
|
|
|
+ document.addEventListener('click', () => {
|
|
|
+ if (!userDropdown.classList.contains('hidden')) {
|
|
|
+ userDropdown.classList.add('hidden');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 头部登录按钮点击
|
|
|
+ headerLoginBtn.addEventListener('click', () => {
|
|
|
+ // 显示登录对话框,默认登录到 novel-platform-user
|
|
|
+ showLoginModal('novel-platform-user');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 退出登录
|
|
|
+ logoutBtn.addEventListener('click', () => {
|
|
|
+ // 清除所有 MCP 的 token
|
|
|
+ for (const mcpType of Object.keys(MCP_SERVERS)) {
|
|
|
+ TokenManager.clearToken(mcpType);
|
|
|
+ }
|
|
|
+ currentUser = null;
|
|
|
+ updateUserMenu();
|
|
|
+ loadMCPServers(); // 刷新 MCP 服务器列表
|
|
|
+ userDropdown.classList.add('hidden');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 初始化用户菜单状态
|
|
|
+ updateUserMenu();
|
|
|
+
|
|
|
// ========== Token 传递到后端 ==========
|
|
|
// 为每个请求添加 Authorization header
|
|
|
async function fetchWithAuth(url, options = {}) {
|