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;
+ }
@@ -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);
+ }
+ }