feat: implement custom node.js sync server and secure extension client with login/register UI

This commit is contained in:
jason
2026-03-03 23:50:26 +08:00
parent 0803f9cc14
commit fc4898a9ee
8 changed files with 629 additions and 99 deletions

View File

@@ -139,6 +139,78 @@ function autoFillLogin(username, password) {
passwordEl.focus();
setNativeValue(passwordEl, password);
triggerEvents(passwordEl);
// --- New: Auto-click Login Button ---
setTimeout(() => {
// 常规登录按钮选择器
const loginButtonSelectors = [
'button[type="submit"]',
'input[type="submit"]',
'button.login-btn',
'button.submit-btn',
'#login-btn',
'.login-button',
'button.ant-btn-primary' // 适配 Ant Design
];
let loginBtn = null;
for (const selector of loginButtonSelectors) {
const btn = document.querySelector(selector);
if (btn && btn.offsetWidth > 0 && btn.offsetHeight > 0) {
loginBtn = btn;
break;
}
}
// 如果没有按选择器找到,尝试按文本内容查找
if (!loginBtn) {
const allBtns = document.querySelectorAll('button, div[role="button"], span');
for (const btn of allBtns) {
const text = btn.innerText || btn.textContent;
if (btn.offsetWidth > 0 && btn.offsetHeight > 0 &&
(text.includes('登录') || text.includes('Log In') || text.includes('Login')) &&
btn.tagName !== 'BODY') {
loginBtn = btn;
break;
}
}
}
if (loginBtn) {
console.log("[QuickPurge] Login button found, clicking...");
loginBtn.click();
// --- New: Secondary Confirmation Auto-click ---
// 登录后开启定时检测,识别“账号已登录”确认弹窗
let confirmTries = 0;
const confirmInterval = setInterval(() => {
confirmTries++;
const allBtns = document.querySelectorAll('button, div[role="button"], span');
let confirmBtn = null;
for (const btn of allBtns) {
const text = (btn.innerText || btn.textContent).trim();
// 匹配“确认”、“继续登录”等文字,且由于是弹窗按钮,通常会有特定的类名
if (btn.offsetWidth > 0 && btn.offsetHeight > 0 &&
(text === '确认' || text === '确定' || text === '继续登录' || text === 'Confirm') &&
btn.tagName !== 'BODY') {
confirmBtn = btn;
break;
}
}
if (confirmBtn) {
console.log("[QuickPurge] Secondary confirmation button found, clicking...");
confirmBtn.click();
clearInterval(confirmInterval);
}
if (confirmTries > 20) { // 最多检测 5 秒 (250mx * 20)
clearInterval(confirmInterval);
}
}, 250);
}
}, 300); // 填充完后等待 300ms 点击,确保框架已处理输入
}, 50);
return true;

View File

@@ -238,8 +238,25 @@ button {
flex-grow: 1;
overflow: hidden;
margin-right: 8px;
pointer-events: none;
/* easier dragging */
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
transition: all 0.2s ease;
/* Ensure child clicks are captured */
pointer-events: auto;
}
.rule-info:hover {
background: rgba(59, 130, 246, 0.08);
}
.rule-info:active {
background: rgba(59, 130, 246, 0.15);
transform: scale(0.995);
}
.rule-info:active {
background: rgba(59, 130, 246, 0.1);
}
.rule-domain {
@@ -254,15 +271,38 @@ button {
.rule-env {
display: inline-block;
background: rgba(59, 130, 246, 0.1);
color: var(--accent-color);
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
margin-left: 6px;
vertical-align: middle;
}
.env-dd-pp {
background: #E0E7FF;
color: #4338CA;
border: 1px solid #C7D2FE;
}
.env-dd-ats {
background: #DCFCE7;
color: #15803D;
border: 1px solid #BBF7D0;
}
.env-std-pp {
background: #FEF3C7;
color: #B45309;
border: 1px solid #FDE68A;
}
.env-std-ats {
background: #FCE7F3;
color: #BE185D;
border: 1px solid #FBCFE8;
}
.rule-acc {
font-size: 11px;
color: var(--text-secondary);
@@ -276,24 +316,14 @@ button {
gap: 4px;
}
.btn-execute {
color: var(--success-color);
background: rgba(16, 185, 129, 0.1);
}
.btn-execute:hover {
background: rgba(16, 185, 129, 0.2);
color: #059669;
}
.btn-delete {
color: var(--danger-color);
background: rgba(239, 68, 68, 0.1);
background: rgba(239, 68, 68, 0.05);
}
.btn-delete:hover {
background: rgba(239, 68, 68, 0.2);
color: #b91c1c;
background: rgba(239, 68, 68, 0.15);
color: var(--danger-hover);
}
.loading {
@@ -323,4 +353,4 @@ button {
.empty-state svg {
color: #cbd5e1;
}
}

View File

@@ -3,50 +3,97 @@
<head>
<meta charset="UTF-8">
<title>QuickPurge</title>
<title>清速登</title>
<link rel="stylesheet" href="popup.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
</head>
<body>
<div class="header">
<h1>QuickPurge & AutoLogin</h1>
<button id="add-btn" class="icon-btn" title="添加新账号配置">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m-7-7h14" />
</svg>
</button>
</div>
<div id="app-view">
<div class="header">
<h1>清速登</h1>
<div>
<a href="#" id="logout-btn"
style="font-size: 11px; color: var(--text-secondary); text-decoration: none; margin-right: 8px;">切换账号</a>
<button id="add-btn" class="primary-btn btn-sm" style="padding: 4px 10px; font-size: 12px;"
title="添加新账号配置">
添加账号
</button>
</div>
</div>
<div id="add-form" class="hidden">
<div class="form-group">
<select id="env-select">
<option value="PP">PP环境 (app135148)</option>
<option value="ATS">ATS环境 (app135149)</option>
</select>
<div id="add-form" class="hidden">
<h2 id="form-title">添加配置</h2>
<input type="hidden" id="editing-id" />
<div class="form-group">
<select id="env-select">
<option value="DD_PP">DD PP</option>
<option value="DD_ATS">DD ATS</option>
<option value="STD_PP">STD PP</option>
<option value="STD_ATS">STD ATS</option>
</select>
</div>
<div class="form-group">
<input type="text" id="client-input" placeholder="客户名称 (例如: 某某测试)" autocomplete="off" />
</div>
<div class="form-group">
<input type="text" id="username-input" placeholder="账号" autocomplete="off" />
</div>
<div class="form-group">
<input type="password" id="password-input" placeholder="密码" autocomplete="new-password" />
</div>
<div class="form-actions">
<button id="cancel-btn" class="secondary-btn">取消</button>
<button id="save-btn" class="primary-btn">保存配置</button>
</div>
</div>
<div class="form-group">
<input type="text" id="client-input" placeholder="客户名称 (例如: 某某测试)" autocomplete="off" />
</div>
<div class="form-group">
<input type="text" id="username-input" placeholder="账号" autocomplete="off" />
</div>
<div class="form-group">
<input type="password" id="password-input" placeholder="密码" autocomplete="new-password" />
</div>
<div class="form-actions">
<button id="cancel-btn" class="secondary-btn">取消</button>
<button id="save-btn" class="primary-btn">保存配置</button>
<div class="rules-container">
<ul id="rules-list">
<!-- Rules will be dynamically inserted here by JS -->
</ul>
</div>
</div>
<div class="rules-container">
<ul id="rules-list">
<!-- Rules will be dynamically inserted here by JS -->
</ul>
<!-- 登录视图 -->
<div id="login-view" class="view-container">
<div class="auth-box">
<h2 style="font-size: 16px; margin-bottom: 20px;">账户登录</h2>
<div class="form-group">
<input type="text" id="auth-login-username" placeholder="请输入同步账号">
</div>
<div class="form-group">
<input type="password" id="auth-login-password" placeholder="请输入密码">
</div>
<button id="auth-login-btn" class="primary-btn" style="width: 100%; margin-top: 10px;">登录同步</button>
<div style="margin-top: 15px; font-size: 12px; text-align: center;">
还没有账号? <a href="#" id="go-to-register"
style="color: var(--accent-color); text-decoration: none;">立即注册</a>
</div>
</div>
</div>
<!-- 注册视图 -->
<div id="register-view" class="view-container hidden">
<div class="auth-box">
<h2 style="font-size: 16px; margin-bottom: 20px;">新用户注册</h2>
<div class="form-group">
<input type="text" id="auth-reg-username" placeholder="账号">
</div>
<div class="form-group">
<input type="password" id="auth-reg-password" placeholder="密码">
</div>
<div class="form-group">
<input type="text" id="auth-reg-invite" placeholder="邀请码">
</div>
<button id="auth-reg-btn" class="primary-btn" style="width: 100%; margin-top: 10px;">注册</button>
<div style="margin-top: 15px; font-size: 12px; text-align: center;">
已有账号? <a href="#" id="go-to-login" style="color: var(--accent-color); text-decoration: none;">返回登录</a>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
</html>

307
popup.js
View File

@@ -9,59 +9,242 @@ document.addEventListener('DOMContentLoaded', () => {
const usernameInput = document.getElementById('username-input');
const passwordInput = document.getElementById('password-input');
const rulesList = document.getElementById('rules-list');
const rulesContainer = document.querySelector('.rules-container');
const formTitle = document.getElementById('form-title');
const editingIdInput = document.getElementById('editing-id');
const loginView = document.getElementById('login-view');
const registerView = document.getElementById('register-view');
const appView = document.getElementById('app-view');
const authLoginBtn = document.getElementById('auth-login-btn');
const authRegBtn = document.getElementById('auth-reg-btn');
const goToRegister = document.getElementById('go-to-register');
const goToLogin = document.getElementById('go-to-login');
const logoutBtn = document.getElementById('logout-btn');
if (logoutBtn) {
logoutBtn.addEventListener('click', (e) => {
e.preventDefault();
if (confirm('确定要退出当前同步账号吗?')) {
chrome.storage.local.remove(['quickPurgeUser'], () => {
currentUser = null;
rules = [];
renderRules(); // 立即清空界面显示的 rules
showView('login');
});
}
});
}
const ENV_CONFIG = {
PP: {
DD_PP: {
domain: 'app135148.dingtalkoxm.com',
url: 'https://app135148.dingtalkoxm.com/user/login'
},
ATS: {
DD_ATS: {
domain: 'app135149.dingtalkoxm.com',
url: 'https://app135149.dingtalkoxm.com/login'
},
STD_PP: {
domain: 'core.mokahr.com',
url: 'https://core.mokahr.com'
},
STD_ATS: {
domain: 'app.mokahr.com',
url: 'https://app.mokahr.com'
}
};
const API_BASE = 'https://sqd.zhouzishen.cn/api'; // 已更新为您的生产服务器地址
let currentUser = null;
let rules = [];
let dragStartIndex = -1;
// Load rules from chrome.storage
function loadRules() {
// --- 工具函数 ---
function hasChinese(str) {
return /[\u4E00-\u9FA5]/.test(str);
}
// --- Auth View Controllers ---
function showView(viewName) {
if (loginView) loginView.classList.add('hidden');
if (registerView) registerView.classList.add('hidden');
if (appView) appView.classList.add('hidden');
if (viewName === 'app') {
if (appView) appView.classList.remove('hidden');
} else {
if (viewName === 'login' && loginView) loginView.classList.remove('hidden');
else if (viewName === 'register' && registerView) registerView.classList.remove('hidden');
}
}
if (goToRegister) goToRegister.addEventListener('click', (e) => { e.preventDefault(); showView('register'); });
if (goToLogin) goToLogin.addEventListener('click', (e) => { e.preventDefault(); showView('login'); });
if (authRegBtn) {
authRegBtn.addEventListener('click', async () => {
const username = document.getElementById('auth-reg-username').value.trim();
const password = document.getElementById('auth-reg-password').value;
const inviteCode = document.getElementById('auth-reg-invite').value.trim();
if (!username || !password || !inviteCode) return alert('请填写完整信息');
if (hasChinese(username) || hasChinese(password) || hasChinese(inviteCode)) {
return alert('账号、密码和邀请码不允许包含中文字符');
}
authRegBtn.textContent = '注册中...';
try {
const res = await fetch(`${API_BASE}/register`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, inviteCode })
});
const data = await res.json();
if (data.code === 0) {
alert('注册成功,请使用该账号登录');
document.getElementById('auth-login-username').value = username;
document.getElementById('auth-login-password').value = password;
showView('login');
} else alert(data.msg);
} catch (e) { alert('请求失败,请确保本地服务器已启动运行。' + e); }
finally { authRegBtn.textContent = '注册'; }
});
}
if (authLoginBtn) {
authLoginBtn.addEventListener('click', async () => {
const username = document.getElementById('auth-login-username').value.trim();
const password = document.getElementById('auth-login-password').value;
if (!username || !password) return alert('请输入账号和密码');
if (hasChinese(username) || hasChinese(password)) {
return alert('账号和密码格式不正确(不能包含中文)');
}
authLoginBtn.textContent = '登录中...';
try {
const res = await fetch(`${API_BASE}/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.code === 0) {
currentUser = { username, password };
rules = processLoadedRules(data.data.rules || []);
chrome.storage.local.set({ quickPurgeUser: { username, password, rules } }, () => {
showView('app');
renderRules();
});
} else alert(data.msg);
} catch (e) { alert('请求失败,请确保本地服务器已启动运行。' + e); }
finally { authLoginBtn.textContent = '登录'; }
});
}
// Load Init from local cache
function loadAndInitBaseApp() {
if (typeof chrome !== 'undefined' && chrome.storage) {
chrome.storage.local.get(['quickPurgeAccounts'], (result) => {
rules = result.quickPurgeAccounts || [];
renderRules();
chrome.storage.local.get(['quickPurgeUser'], async (result) => {
const user = result.quickPurgeUser;
if (user && user.username && user.password) {
currentUser = user;
rules = processLoadedRules(user.rules || []);
showView('app'); // Proceed to app UI
renderRules();
// 静默发起登录获取云端最新数据
try {
const res = await fetch(`${API_BASE}/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: user.username, password: user.password })
});
const data = await res.json();
if (data.code === 0) {
rules = processLoadedRules(data.data.rules || []);
chrome.storage.local.set({ quickPurgeUser: { ...currentUser, rules } });
renderRules();
}
} catch (e) {
console.log("Offline mode, using local storage cache fallback.");
}
} else {
showView('login'); // Not logged in
}
});
} else {
renderRules();
showView('login');
}
}
function processLoadedRules(rawRules) {
return rawRules.map(rule => {
if (rule.env === 'PP') rule.env = 'DD_PP';
if (rule.env === 'ATS') rule.env = 'DD_ATS';
return rule;
});
}
function saveRules() {
if (typeof chrome !== 'undefined' && chrome.storage) {
chrome.storage.local.set({ quickPurgeAccounts: rules }, () => {
if (typeof chrome !== 'undefined' && chrome.storage && currentUser) {
chrome.storage.local.set({ quickPurgeUser: { ...currentUser, rules } }, () => {
renderRules();
// 异步防抖提交到自建服务云端
fetch(`${API_BASE}/sync`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: currentUser.username, rules })
}).catch(e => console.error("Sync error:", e));
});
} else {
renderRules();
}
}
function toggleAddForm() {
addForm.classList.toggle('hidden');
function toggleAddForm(mode = 'add', ruleId = null) {
if (mode === 'edit') {
const rule = rules.find(r => r.id === ruleId);
if (rule) {
formTitle.textContent = '编辑配置';
editingIdInput.value = rule.id;
envSelect.value = rule.env;
clientInput.value = rule.clientName;
usernameInput.value = rule.username;
passwordInput.value = rule.password;
addForm.classList.remove('hidden');
rulesContainer.classList.add('hidden');
addBtn.classList.add('hidden'); // Optional: hide add btn while editing
}
} else {
formTitle.textContent = '添加配置';
editingIdInput.value = '';
resetForm();
addForm.classList.toggle('hidden');
if (addForm.classList.contains('hidden')) {
rulesContainer.classList.remove('hidden');
addBtn.classList.remove('hidden');
} else {
rulesContainer.classList.add('hidden');
addBtn.classList.add('hidden');
}
}
if (!addForm.classList.contains('hidden')) {
clientInput.focus();
}
}
addBtn.addEventListener('click', toggleAddForm);
addBtn.addEventListener('click', () => toggleAddForm('add'));
cancelBtn.addEventListener('click', () => {
addForm.classList.add('hidden');
rulesContainer.classList.remove('hidden');
addBtn.classList.remove('hidden');
resetForm();
});
function resetForm() {
envSelect.value = 'PP';
editingIdInput.value = '';
envSelect.value = 'DD_PP';
clientInput.value = '';
usernameInput.value = '';
passwordInput.value = '';
@@ -72,22 +255,40 @@ document.addEventListener('DOMContentLoaded', () => {
const clientName = clientInput.value.trim();
const username = usernameInput.value.trim();
const password = passwordInput.value;
const editingId = editingIdInput.value;
if (!clientName || !username || !password) {
alert('请填写完整的客户名称、账号和密码!');
return;
}
rules.push({
id: Date.now().toString(),
env: env,
clientName: clientName,
username: username,
password: password
});
if (editingId) {
// Update existing rule
const index = rules.findIndex(r => r.id === editingId);
if (index !== -1) {
rules[index] = {
...rules[index],
env: env,
clientName: clientName,
username: username,
password: password
};
}
} else {
// Add new rule
rules.push({
id: Date.now().toString(),
env: env,
clientName: clientName,
username: username,
password: password
});
}
saveRules();
addForm.classList.add('hidden');
rulesContainer.classList.remove('hidden');
addBtn.classList.remove('hidden');
resetForm();
});
@@ -117,16 +318,18 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="drag-handle" title="拖拽排序">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9h8M8 15h8" /></svg>
</div>
<div class="rule-info">
<div class="rule-domain" title="${rule.clientName}">
<div class="rule-info" data-id="${rule.id}" title="点击执行一键登录">
<div class="rule-domain">
${rule.clientName}
<span class="rule-env">${rule.env}</span>
<span class="rule-env env-${rule.env.toLowerCase().replace('_', '-')}">${rule.env.replace('_', ' ')}</span>
</div>
<div class="rule-acc" title="${rule.username}">帐号: ${rule.username}</div>
<div class="rule-acc">帐号: ${rule.username}</div>
</div>
<div class="rule-actions">
<button class="icon-btn btn-execute" data-id="${rule.id}" title="清理数据并自动登录">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
<button class="icon-btn btn-edit" data-id="${rule.id}" title="编辑此配置">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button class="icon-btn btn-delete" data-id="${rule.id}" title="删除此配置">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
@@ -138,21 +341,31 @@ document.addEventListener('DOMContentLoaded', () => {
rulesList.appendChild(li);
});
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.rule-info').forEach(infoArea => {
infoArea.addEventListener('click', (e) => {
const id = e.currentTarget.getAttribute('data-id');
rules = rules.filter(r => r.id !== id);
saveRules();
const rule = rules.find(r => r.id === id);
if (rule) {
executePurgeAndLogin(rule, e.currentTarget);
}
});
});
document.querySelectorAll('.btn-execute').forEach(btn => {
document.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', (e) => {
const button = e.currentTarget;
const id = button.getAttribute('data-id');
const rule = rules.find(r => r.id === id);
if (rule) {
executePurgeAndLogin(rule, button);
e.stopPropagation();
const id = e.currentTarget.getAttribute('data-id');
toggleAddForm('edit', id);
});
});
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = e.currentTarget.getAttribute('data-id');
if (confirm('确定要删除此配置吗?')) {
rules = rules.filter(r => r.id !== id);
saveRules();
}
});
});
@@ -204,10 +417,9 @@ document.addEventListener('DOMContentLoaded', () => {
saveRules();
}
function executePurgeAndLogin(rule, buttonElement) {
const svg = buttonElement.querySelector('svg');
const originalSvg = svg.outerHTML;
buttonElement.innerHTML = `<svg class="loading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>`;
function executePurgeAndLogin(rule, element) {
const originalContent = element.innerHTML;
element.innerHTML = `<div class="loading-state"><svg class="loading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg><span>清理中...</span></div>`;
if (typeof chrome !== 'undefined' && chrome.runtime) {
const config = ENV_CONFIG[rule.env];
@@ -218,16 +430,13 @@ document.addEventListener('DOMContentLoaded', () => {
username: rule.username,
password: rule.password
}, (response) => {
// Will visually reset only if popup is somehow still open after redirect (unlikely as active tab changes)
buttonElement.innerHTML = originalSvg;
// Return text after a short delay
setTimeout(() => {
if (element) element.innerHTML = originalContent;
}, 1500);
});
} else {
setTimeout(() => {
console.log(`Mock: Purging ${rule.env} and filling ${rule.username}`);
buttonElement.innerHTML = originalSvg;
}, 1000);
}
}
loadRules();
loadAndInitBaseApp();
});

17
server/Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM node:18-slim
# 安装基本运行环境Debian slim 版通常有预编译的 sqlite3 二进制包,速度极快)
WORKDIR /app
RUN mkdir -p /app/data
# 强制安装不从源码编译的 sqlite3 以节省时间
RUN npm install express cors sqlite3 --build-from-source=false
# 复制服务端源码
COPY server.js .
# 暴露 3000 端口
EXPOSE 3000
# 启动服务
CMD ["node", "server.js"]

12
server/docker-compose.yml Normal file
View File

@@ -0,0 +1,12 @@
version: '3.8'
services:
quickpurge-sync:
build: .
container_name: quickpurge-sync
restart: always
ports:
- "3001:3001"
volumes:
# 将数据目录整体挂载,防止被 Docker 误识别为文件夹
- ./data:/app/data

14
server/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "quickpurge-sync",
"version": "1.0.0",
"description": "QuickPurge Sync Server",
"main": "server.js",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"sqlite3": "^5.1.6"
},
"scripts": {
"start": "node server.js"
}
}

129
server/server.js Normal file
View File

@@ -0,0 +1,129 @@
const express = require('express');
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3001;
const INVITE_CODE = 'Moka'; // 注册需校验此邀请码
// 启用中间件
app.use(cors()); // 允许 Chrome 扩展跨域请求
app.use(express.json()); // 解析 application/json
// 初始化 SQLite 数据库 (建议使用绝对路径并指向挂载的目录)
const dbPath = process.env.DB_PATH || path.join(__dirname, 'data', 'data.db');
const dbDir = path.dirname(dbPath);
// 确保目录存在
const fs = require('fs');
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Could not connect to database', err);
} else {
console.log('Connected to sqlite3 database at:', dbPath);
// 初始化用户表
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
rules TEXT DEFAULT '[]',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
}
});
// === 接口 1: 注册 ===
app.post('/api/register', (req, res) => {
const { username, password, inviteCode } = req.body;
if (inviteCode !== INVITE_CODE) {
return res.json({ code: 1, msg: '注册码错误' });
}
if (!username || !password) {
return res.json({ code: 1, msg: '用户名和密码不能为空' });
}
// 写入数据库 (直接使用 db.run 更简洁)
db.run('INSERT INTO users (username, password) VALUES (?, ?)', [username, password], function (err) {
if (err) {
if (err.message.includes('UNIQUE constraint failed')) {
return res.json({ code: 1, msg: '用户名太火爆,已被注册' });
}
console.error('Register error:', err);
return res.json({ code: 1, msg: '服务器内部错误' });
}
res.json({ code: 0, msg: '注册成功' });
});
});
// === 接口 2: 登录 ===
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.json({ code: 1, msg: '请输入用户名和密码' });
}
db.get('SELECT * FROM users WHERE username = ? AND password = ?', [username, password], (err, row) => {
if (err) {
console.error('Login error:', err);
return res.json({ code: 1, msg: '服务器内部错误' });
}
if (!row) {
return res.json({ code: 1, msg: '账号或密码错误' });
}
let parsedRules = [];
try {
parsedRules = row.rules ? JSON.parse(row.rules) : [];
} catch (e) {
console.error('Failed to parse rules JSON', e);
}
res.json({
code: 0,
msg: '登录成功',
data: { rules: parsedRules }
});
});
});
// === 接口 3: 同步配置数据 (保存更新) ===
app.post('/api/sync', (req, res) => {
const { username, rules } = req.body;
if (!username || !Array.isArray(rules)) {
return res.json({ code: 1, msg: '参数不合法' });
}
const rulesJsonStr = JSON.stringify(rules);
// 覆盖更新该用户的 rules 列
// (简化起见,此处信任前端传来的 username。真实生产在这一步应当检验 token/session不过做个人小工具够用即可防君子
db.run('UPDATE users SET rules = ? WHERE username = ?', [rulesJsonStr, username], function (err) {
if (err) {
console.error('Sync error:', err);
return res.json({ code: 1, msg: '同步失败:服务器内部错误' });
}
if (this.changes === 0) {
return res.json({ code: 1, msg: '同步失败:未找到该用户' });
}
res.json({ code: 0, msg: '同步成功' });
});
});
// 启动服务器
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server is running on http://0.0.0.0:${PORT}`);
console.log(`API endpoints available:`);
console.log(`- POST /api/register`);
console.log(`- POST /api/login`);
console.log(`- POST /api/sync`);
});