diff --git a/hubcmdui/README.md b/hubcmdui/README.md index 54401fc..591f14d 100644 --- a/hubcmdui/README.md +++ b/hubcmdui/README.md @@ -105,7 +105,7 @@ docker logs -f [容器ID或名称] - +
diff --git a/hubcmdui/config.json b/hubcmdui/config.json index a1e8c36..ba7308d 100644 --- a/hubcmdui/config.json +++ b/hubcmdui/config.json @@ -1,6 +1,5 @@ { "logo": "", - "proxyDomain": "dqzboy.github.io", "menuItems": [ { "text": "首页", @@ -8,10 +7,16 @@ "newTab": false }, { - "text": "项目", + "text": "GitHub", "link": "https://github.com/dqzboy/Docker-Proxy", "newTab": true } ], - "adImages": [] + "adImages": [ + { + "url": "https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/guanggao.png", + "link": "https://www.dqzboy.com" + } + ], + "proxyDomain": "dqzboy.github.io" } \ No newline at end of file diff --git a/hubcmdui/package.json b/hubcmdui/package.json index fe22126..8175ae0 100644 --- a/hubcmdui/package.json +++ b/hubcmdui/package.json @@ -1,6 +1,8 @@ { "dependencies": { + "bcrypt": "^5.1.1", "express": "^4.19.2", - "express-session": "^1.18.0" + "express-session": "^1.18.0", + "morgan": "^1.10.0" } } diff --git a/hubcmdui/server.js b/hubcmdui/server.js index 79a40e3..268caf2 100644 --- a/hubcmdui/server.js +++ b/hubcmdui/server.js @@ -3,6 +3,9 @@ const fs = require('fs').promises; const path = require('path'); const bodyParser = require('body-parser'); const session = require('express-session'); +const bcrypt = require('bcrypt'); +const crypto = require('crypto'); +const logger = require('morgan'); // 引入 morgan 作为日志工具 const app = express(); app.use(express.json()); @@ -14,6 +17,7 @@ app.use(session({ saveUninitialized: true, cookie: { secure: false } // 设置为true如果使用HTTPS })); +app.use(logger('dev')); // 使用 morgan 记录请求日志 app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'web', 'admin.html')); @@ -32,7 +36,7 @@ async function readConfig() { return { logo: '', menuItems: [], - adImage: { url: '', link: '' } + adImages: [] }; } console.log('Config read successfully'); @@ -43,7 +47,7 @@ async function readConfig() { return { logo: '', menuItems: [], - adImage: { url: '', link: '' } + adImages: [] }; } throw error; @@ -68,9 +72,10 @@ async function readUsers() { return JSON.parse(data); } catch (error) { if (error.code === 'ENOENT') { - return { - users: [{ username: 'root', password: 'admin' }] - }; + console.warn('Users file does not exist, creating default user'); + const defaultUser = { username: 'root', password: bcrypt.hashSync('admin', 10) }; + await writeUsers([defaultUser]); + return { users: [defaultUser] }; } throw error; } @@ -78,19 +83,35 @@ async function readUsers() { // 写入用户 async function writeUsers(users) { - await fs.writeFile(USERS_FILE, JSON.stringify(users, null, 2), 'utf8'); + await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8'); } // 登录验证 app.post('/api/login', async (req, res) => { - const { username, password } = req.body; + const { username, password, captcha } = req.body; + console.log(`Received login request for user: ${username}`); // 打印登录请求的用户名 + + if (req.session.captcha !== parseInt(captcha)) { + console.log(`Captcha verification failed for user: ${username}`); // 打印验证码验证失败 + return res.status(401).json({ error: '验证码错误' }); + } + const users = await readUsers(); - const user = users.users.find(u => u.username === username && u.password === password); - if (user) { + const user = users.users.find(u => u.username === username); + + if (!user) { + console.log(`User ${username} not found`); // 打印用户未找到 + return res.status(401).json({ error: '用户名或密码错误' }); + } + + console.log(`User ${username} found, comparing passwords`); // 打印用户找到,开始比较密码 + if (bcrypt.compareSync(password, user.password)) { + console.log(`User ${username} logged in successfully`); // 打印登录成功 req.session.user = user; res.json({ success: true }); } else { - res.status(401).json({ error: 'Invalid credentials' }); + console.log(`Login failed for user: ${username}, password mismatch`); // 打印密码不匹配 + res.status(401).json({ error: '用户名或密码错误' }); } }); @@ -100,11 +121,14 @@ app.post('/api/change-password', async (req, res) => { return res.status(401).json({ error: 'Not logged in' }); } const { currentPassword, newPassword } = req.body; + if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(newPassword)) { + return res.status(400).json({ error: 'Password must be 8-16 characters long and contain at least one letter and one number' }); + } const users = await readUsers(); const user = users.users.find(u => u.username === req.session.user.username); - if (user && user.password === currentPassword) { - user.password = newPassword; - await writeUsers(users); + if (user && bcrypt.compareSync(currentPassword, user.password)) { + user.password = bcrypt.hashSync(newPassword, 10); + await writeUsers(users.users); res.json({ success: true }); } else { res.status(401).json({ error: 'Invalid current password' }); @@ -133,10 +157,12 @@ app.get('/api/config', async (req, res) => { // API 端点:保存配置 app.post('/api/config', requireLogin, async (req, res) => { try { - await writeConfig(req.body); - res.json({ success: true }); + const currentConfig = await readConfig(); + const newConfig = { ...currentConfig, ...req.body }; + await writeConfig(newConfig); + res.json({ success: true }); } catch (error) { - res.status(500).json({ error: 'Failed to save config' }); + res.status(500).json({ error: 'Failed to save config' }); } }); @@ -149,6 +175,15 @@ app.get('/api/check-session', (req, res) => { } }); +// API 端点:生成验证码 +app.get('/api/captcha', (req, res) => { + const num1 = Math.floor(Math.random() * 10); + const num2 = Math.floor(Math.random() * 10); + const captcha = `${num1} + ${num2} = ?`; + req.session.captcha = num1 + num2; + res.json({ captcha }); +}); + // 启动服务器 const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/hubcmdui/users.json b/hubcmdui/users.json index 8759e56..fd6d461 100644 --- a/hubcmdui/users.json +++ b/hubcmdui/users.json @@ -2,7 +2,7 @@ "users": [ { "username": "root", - "password": "admin" + "password": "$2b$10$wKdemJNjB1I6IpOycHWjwO2MgDFj3QC6KLSMxZE6rHIofuSf.BX/m" } ] } \ No newline at end of file diff --git a/hubcmdui/web/admin.html b/hubcmdui/web/admin.html index c5fdb58..084a205 100644 --- a/hubcmdui/web/admin.html +++ b/hubcmdui/web/admin.html @@ -157,18 +157,24 @@ font-size: 24px; color: #0366d6; } + .password-hint { + color: gray; + font-size: 12px; + margin-top: 5px; + }
@@ -222,6 +228,11 @@ + +
+ + 点击刷新验证码 +
@@ -264,7 +275,8 @@ if (text) { const rowIndex = row.getAttribute('data-index'); menuItems[rowIndex] = { text, link, newTab }; - renderMenuItems(); + saveMenuItem(rowIndex, { text, link, newTab }); + renderMenuItems(); // 重新渲染菜单项 } else { alert('请填写菜单项文本'); } @@ -290,13 +302,11 @@ - `; tbody.innerHTML += row; }); - setupDragAndDrop(); setupEditButtons(); setupDeleteButtons(); } @@ -308,8 +318,7 @@ button.addEventListener('click', () => { const row = button.closest('tr'); const index = row.getAttribute('data-index'); - menuItems.splice(index, 1); - renderMenuItems(); + deleteMenuItem(index); }); }); } @@ -347,8 +356,10 @@ const newTab = newTabSelect.value === 'true'; if (text) { - menuItems.push({ text, link, newTab }); // 确保新菜单项被添加到 menuItems 数组中 - renderMenuItems(); + const newItem = { text, link, newTab }; + menuItems.push(newItem); + renderMenuItems(); // 先更新页面 + saveMenuItem(menuItems.length - 1, newItem); cancelNewMenuItem(); } else { alert('请填写菜单项文本'); @@ -402,8 +413,10 @@ const url = document.getElementById('newAdUrl').value || ''; const link = document.getElementById('newAdLink').value || ''; - adImages.push({ url, link }); - renderAdItems(); + const newAd = { url, link }; + adImages.push(newAd); + renderAdItems(); // 先更新页面 + saveAd(adImages.length - 1, newAd); cancelNewAd(); } @@ -452,7 +465,8 @@ const link = linkInput.value || ''; adImages[editingIndex] = { url, link }; - renderAdItems(); + renderAdItems(); // 重新渲染广告项 + saveAd(editingIndex, { url, link }); editingIndex = -1; } }); @@ -465,33 +479,76 @@ button.addEventListener('click', () => { const row = button.closest('tr'); const index = row.getAttribute('data-index'); - adImages.splice(index, 1); - renderAdItems(); + deleteAd(index); }); }); } - async function saveConfig() { - const config = { - logo: document.getElementById('logoUrl').value, - proxyDomain: document.getElementById('proxyDomain').value, - menuItems: menuItems, - adImages: adImages - }; + async function saveLogo() { + const logoUrl = document.getElementById('logoUrl').value; + if (!logoUrl) { + alert('Logo URL 不可为空'); + return; + } + try { + await saveConfig({ logo: logoUrl }); + alert('Logo 保存成功'); + } catch (error) { + alert('Logo 保存失败: ' + error.message); + } + } + async function saveProxyDomain() { + const proxyDomain = document.getElementById('proxyDomain').value; + if (!proxyDomain) { + alert('Docker镜像代理地址不可为空'); + return; + } + try { + await saveConfig({ proxyDomain }); + alert('代理地址保存成功'); + } catch (error) { + alert('代理地址保存失败: ' + error.message); + } + } + + async function saveMenuItem(index, item) { + const config = { menuItems: menuItems }; + config.menuItems[index] = item; + await saveConfig(config); + } + + async function deleteMenuItem(index) { + menuItems.splice(index, 1); + renderMenuItems(); // 先更新页面 + await saveConfig({ menuItems: menuItems }); + } + + async function saveAd(index, ad) { + const config = { adImages: adImages }; + config.adImages[index] = ad; + await saveConfig(config); + } + + async function deleteAd(index) { + adImages.splice(index, 1); + renderAdItems(); // 先更新页面 + await saveConfig({ adImages: adImages }); + } + + async function saveConfig(partialConfig) { try { const response = await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(config) + body: JSON.stringify(partialConfig) }); - if (response.ok) { - alert('配置已保存'); - } else { + if (!response.ok) { throw new Error('保存失败'); } } catch (error) { - alert('保存失败: ' + error.message); + console.error('保存失败: ' + error.message); + throw error; } } @@ -512,11 +569,12 @@ async function login() { const username = document.getElementById('username').value; const password = document.getElementById('password').value; + const captcha = document.getElementById('captcha').value; try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }) + body: JSON.stringify({ username, password, captcha }) }); if (response.ok) { isLoggedIn = true; @@ -525,7 +583,8 @@ document.getElementById('adminContainer').classList.remove('hidden'); loadConfig(); } else { - alert('登录失败'); + const errorData = await response.json(); + alert(errorData.error); } } catch (error) { alert('登录失败: ' + error.message); @@ -539,6 +598,10 @@ alert('请填写当前密码和新密码'); return; } + if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(newPassword)) { + alert('密码必须包含至少一个字母和一个数字,长度在8到16个字符之间'); + return; + } try { const response = await fetch('/api/change-password', { method: 'POST', @@ -555,6 +618,16 @@ } } + function checkPasswordStrength() { + const newPassword = document.getElementById('newPassword'); + const passwordHint = document.getElementById('passwordHint'); + if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(newPassword.value)) { + passwordHint.style.display = 'block'; + } else { + passwordHint.style.display = 'none'; + } + } + // 页面加载时检查登录状态 window.onload = async function() { try { @@ -567,39 +640,20 @@ loadConfig(); } else { document.getElementById('loginModal').style.display = 'block'; + refreshCaptcha(); } } else { localStorage.removeItem('isLoggedIn'); document.getElementById('loginModal').style.display = 'block'; + refreshCaptcha(); } } catch (error) { localStorage.removeItem('isLoggedIn'); document.getElementById('loginModal').style.display = 'block'; + refreshCaptcha(); } }; - // 表单提交事件监听器 - document.getElementById('adminForm').addEventListener('submit', async function(e) { - e.preventDefault(); - await saveConfig(); - }); - - function setupDragAndDrop() { - const drake = dragula([document.getElementById('menuTableBody')], { - moves: function (el, container, handle) { - return handle.classList.contains('drag-handle'); - } - }); - - drake.on('drop', (el, target, source, sibling) => { - const newIndex = Array.from(target.children).indexOf(el); - const oldIndex = el.getAttribute('data-index'); - const movedItem = menuItems.splice(oldIndex, 1)[0]; - menuItems.splice(newIndex, 0, movedItem); - renderMenuItems(); - }); - } - function updateAdImage(adImages) { const adContainer = document.getElementById('adContainer'); adContainer.innerHTML = ''; @@ -630,6 +684,16 @@ }, 5000); // 每5秒切换一次广告 } } + + async function refreshCaptcha() { + try { + const response = await fetch('/api/captcha'); + const data = await response.json(); + document.getElementById('captchaText').textContent = data.captcha; + } catch (error) { + console.error('刷新验证码失败:', error); + } + } \ No newline at end of file diff --git a/install/DockerProxy_Install.sh b/install/DockerProxy_Install.sh index d9059f6..a99e3b7 100644 --- a/install/DockerProxy_Install.sh +++ b/install/DockerProxy_Install.sh @@ -1844,6 +1844,31 @@ INSTALL_HUBCMDUI() { fi } + +UPDATE_HUBCMDUI() { + if [ -d "${CMDUI_DIR}" ]; then + if [ -f "${CMDUI_DIR}/${DOCKER_COMPOSE_FILE}" ]; then + INFO "正在更新HubCMD-UI容器" + docker-compose -f "${CMDUI_DIR}/${DOCKER_COMPOSE_FILE}" pull + if [ $? -ne 0 ]; then + WARN "HubCMD-UI ${LIGHT_YELLOW}镜像拉取失败${RESET},请稍后重试!" + HUBCMDUI + fi + docker-compose -f "${CMDUI_DIR}/${DOCKER_COMPOSE_FILE}" up -d --force-recreate + if [ $? -ne 0 ]; then + WARN "HubCMD-UI ${LIGHT_YELLOW}服务启动失败${RESET},请稍后重试!" + HUBCMDUI + else + INFO "HubCMD-UI ${LIGHT_GREEN}服务更新并启动完成${RESET}" + fi + else + WARN "${LIGHT_YELLOW}文件${CMDUI_DIR}/${DOCKER_COMPOSE_FILE} 不存在,无法进行更新操作!${RESET}" + fi + else + WARN "${LIGHT_YELLOW}目录 ${CMDUI_DIR} 不存在,无法进行更新操作!${RESET}" + fi +} + UNINSTALL_HUBCMDUI() { WARN "${LIGHT_RED}注意:${RESET} ${LIGHT_YELLOW}请执行删除之前确定是否需要备份配置文件${RESET}" while true; do @@ -1865,7 +1890,8 @@ done SEPARATOR "HubCMD-UI管理" echo -e "1) ${BOLD}${LIGHT_GREEN}安装${RESET}HubCMD-UI" echo -e "2) ${BOLD}${LIGHT_YELLOW}卸载${RESET}HubCMD-UI" -echo -e "3) ${BOLD}返回${LIGHT_RED}主菜单${RESET}" +echo -e "3) ${BOLD}${LIGHT_CYAN}更新${RESET}HubCMD-UI" +echo -e "4) ${BOLD}返回${LIGHT_RED}主菜单${RESET}" echo -e "0) ${BOLD}退出脚本${RESET}" echo "---------------------------------------------------------------" read -e -p "$(INFO "输入${LIGHT_CYAN}对应数字${RESET}并按${LIGHT_GREEN}Enter${RESET}键 > ")" cmdui_choice @@ -1880,13 +1906,17 @@ case $cmdui_choice in HUBCMDUI ;; 3) + UPDATE_HUBCMDUI + HUBCMDUI + ;; + 4) main_menu ;; 0) exit 1 ;; *) - WARN "输入了无效的选择。请重新${LIGHT_GREEN}选择0-8${RESET}的选项." + WARN "输入了无效的选择。请重新${LIGHT_GREEN}选择0-4${RESET}的选项." HUBCMDUI ;; esac