feat: Add login verification and UI interface optimization, support one-click update version.

This commit is contained in:
dqzboy
2024-07-26 02:53:25 +08:00
parent 17ba7cffd5
commit 9364d7bfe9
7 changed files with 221 additions and 85 deletions

View File

@@ -105,7 +105,7 @@ docker logs -f [容器ID或名称]
<table>
<tr>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/d2f76296-e329-4941-9292-8d3d43e2bea4"?raw=true"></td>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/d5d97581-42c1-4a21-9c3a-fddf3bfc67cd"?raw=true"></td>
</tr>
</table>

View File

@@ -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"
}

View File

@@ -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"
}
}

View File

@@ -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, () => {

View File

@@ -2,7 +2,7 @@
"users": [
{
"username": "root",
"password": "admin"
"password": "$2b$10$wKdemJNjB1I6IpOycHWjwO2MgDFj3QC6KLSMxZE6rHIofuSf.BX/m"
}
]
}

View File

@@ -157,18 +157,24 @@
font-size: 24px;
color: #0366d6;
}
.password-hint {
color: gray;
font-size: 12px;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container hidden" id="adminContainer">
<h1 class="admin-title">Docker 镜像代理加速 - 管理面板</h1>
<p></h1>配置添加或修改后,点击【保存更改】保存配置</p>
<form id="adminForm">
<label for="logoUrl">Logo URL: (可选)</label>
<input type="url" id="logoUrl" name="logoUrl">
<button type="button" onclick="saveLogo()">保存 Logo</button>
<label for="proxyDomain">Docker镜像代理地址: (必填)</label>
<input type="text" id="proxyDomain" name="proxyDomain" required>
<button type="button" onclick="saveProxyDomain()">保存代理地址</button>
<h2 class="menu-label">菜单项管理</h2>
<table id="menuTable">
@@ -200,19 +206,19 @@
</tbody>
</table>
<button type="button" class="add-btn" onclick="showNewAdRow()">添加广告</button>
<button type="submit">保存更改</button>
</form>
<!-- 修改密码的独立表单 -->
<div id="passwordChangeForm" style="margin-top: 20px;">
<h2 class="menu-label">修改密码</h2>
<label for="currentPassword">当前密码</label>
<input type="password" id="currentPassword" name="currentPassword">
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" name="newPassword">
<button type="button" onclick="changePassword()">修改密码</button>
</div>
<!-- 修改密码的独立表单 -->
<div id="passwordChangeForm" style="margin-top: 20px;">
<h2 class="menu-label">修改密码</h2>
<label for="currentPassword">当前密码</label>
<input type="password" id="currentPassword" name="currentPassword">
<label for="newPassword">新密码</label>
<span class="password-hint" id="passwordHint">密码必须包含至少一个字母和一个数字长度在8到16个字符之间</span>
<input type="password" id="newPassword" name="newPassword" oninput="checkPasswordStrength()">
<span id="passwordStrength" style="color: red;"></span>
<button type="button" onclick="changePassword()">修改密码</button>
</div>
</form>
</div>
<div class="login-modal" id="loginModal">
@@ -222,6 +228,11 @@
<input type="text" id="username" name="username" required>
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
<label for="captcha">验证码</label>
<div style="display: flex; align-items: center;">
<input type="text" id="captcha" name="captcha" required style="flex: 1;">
<span id="captchaText" onclick="refreshCaptcha()" style="margin-left: 10px; cursor: pointer;">点击刷新验证码</span>
</div>
<button type="button" onclick="login()">登录</button>
</div>
</div>
@@ -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 @@
<td>
<button type="button" class="action-btn edit-btn">编辑</button>
<button type="button" class="action-btn delete-btn">删除</button>
<span class="drag-handle" style="cursor: move;">☰</span>
</td>
</tr>
`;
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);
}
}
</script>
</body>
</html>