feat: implement custom node.js sync server and secure extension client with login/register UI
This commit is contained in:
307
popup.js
307
popup.js
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user