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

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();
});