mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-01-12 16:25:42 +08:00
feat: Add login verification and UI interface optimization, support one-click update version.
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"users": [
|
||||
{
|
||||
"username": "root",
|
||||
"password": "admin"
|
||||
"password": "$2b$10$wKdemJNjB1I6IpOycHWjwO2MgDFj3QC6KLSMxZE6rHIofuSf.BX/m"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user