feat: 实现便携版并增加一键导出功能
This commit is contained in:
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# EditorConfig helps maintain consistent coding styles
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{js,jsx,ts,tsx,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Database and user data (Portable data)
|
||||||
|
accounts.db
|
||||||
|
data/
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# Exports (Contains sensitive information)
|
||||||
|
accounts_export.txt
|
||||||
|
export.js
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Production build (if you pack the app later)
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
107
main.js
Normal file
107
main.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const { app, BrowserWindow, ipcMain, clipboard, Menu } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const Database = require('./src/db/database');
|
||||||
|
|
||||||
|
// 强制设置用户数据存储位置为项目根目录下的 data 文件夹
|
||||||
|
// 这样 Electron 的缓存、Localstorage、日志等都不会写到 C 盘
|
||||||
|
app.setPath('userData', path.join(process.cwd(), 'data'));
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
let db;
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// 移除应用菜单栏
|
||||||
|
Menu.setApplicationMenu(null);
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 900,
|
||||||
|
minHeight: 600,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false
|
||||||
|
},
|
||||||
|
backgroundColor: '#0a0a0b',
|
||||||
|
titleBarStyle: 'default',
|
||||||
|
show: false
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadFile('src/index.html');
|
||||||
|
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开发时打开DevTools
|
||||||
|
// mainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
db = new Database();
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== Vault IPC Handlers ====================
|
||||||
|
ipcMain.handle('db:getVaults', async () => {
|
||||||
|
return db.getVaults();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:addVault', async (event, vault) => {
|
||||||
|
return db.addVault(vault);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:updateVault', async (event, id, vault) => {
|
||||||
|
return db.updateVault(id, vault);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:deleteVault', async (event, id) => {
|
||||||
|
return db.deleteVault(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== Account IPC Handlers ====================
|
||||||
|
ipcMain.handle('db:getAccounts', async (event, page, limit, vaultId) => {
|
||||||
|
return db.getAccounts(page, limit, vaultId);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:getAccountCount', async (event, vaultId) => {
|
||||||
|
return db.getAccountCount(vaultId);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:addAccount', async (event, account) => {
|
||||||
|
return db.addAccount(account);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:updateAccount', async (event, id, account) => {
|
||||||
|
return db.updateAccount(id, account);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:deleteAccount', async (event, id) => {
|
||||||
|
return db.deleteAccount(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:moveAccountToVault', async (event, accountId, vaultId) => {
|
||||||
|
return db.moveAccountToVault(accountId, vaultId);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('db:searchAccounts', async (event, query, vaultId) => {
|
||||||
|
return db.searchAccounts(query, vaultId);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('clipboard:write', async (event, text) => {
|
||||||
|
clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
888
package-lock.json
generated
Normal file
888
package-lock.json
generated
Normal file
@@ -0,0 +1,888 @@
|
|||||||
|
{
|
||||||
|
"name": "2fa-account-tool",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "2fa-account-tool",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"sql.js": "^1.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^28.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/get": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"env-paths": "^2.2.0",
|
||||||
|
"fs-extra": "^8.1.0",
|
||||||
|
"got": "^11.8.5",
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"semver": "^6.2.0",
|
||||||
|
"sumchecker": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"global-agent": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sindresorhus/is": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@szmarczak/http-timer": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"defer-to-connect": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/cacheable-request": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/http-cache-semantics": "*",
|
||||||
|
"@types/keyv": "^3.1.4",
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/responselike": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/http-cache-semantics": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/keyv": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "18.19.130",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||||
|
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/responselike": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/yauzl": {
|
||||||
|
"version": "2.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
|
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boolean": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable-lookup": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable-request": {
|
||||||
|
"version": "7.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||||
|
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clone-response": "^1.0.2",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"http-cache-semantics": "^4.0.0",
|
||||||
|
"keyv": "^4.0.0",
|
||||||
|
"lowercase-keys": "^2.0.0",
|
||||||
|
"normalize-url": "^6.0.1",
|
||||||
|
"responselike": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clone-response": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/defer-to-connect": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-properties": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-node": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/electron": {
|
||||||
|
"version": "28.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz",
|
||||||
|
"integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@electron/get": "^2.0.0",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"extract-zip": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"electron": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.20.55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/env-paths": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-error": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/extract-zip": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"yauzl": "^2.10.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"extract-zip": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.17.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/yauzl": "^2.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6 <7 || >=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pump": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-agent": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"boolean": "^3.0.1",
|
||||||
|
"es6-error": "^4.1.1",
|
||||||
|
"matcher": "^3.0.0",
|
||||||
|
"roarr": "^2.15.3",
|
||||||
|
"semver": "^7.3.2",
|
||||||
|
"serialize-error": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-agent/node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/globalthis": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"define-properties": "^1.2.1",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/got": {
|
||||||
|
"version": "11.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||||
|
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sindresorhus/is": "^4.0.0",
|
||||||
|
"@szmarczak/http-timer": "^4.0.5",
|
||||||
|
"@types/cacheable-request": "^6.0.1",
|
||||||
|
"@types/responselike": "^1.0.0",
|
||||||
|
"cacheable-lookup": "^5.0.3",
|
||||||
|
"cacheable-request": "^7.0.2",
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||||
|
"lowercase-keys": "^2.0.0",
|
||||||
|
"p-cancelable": "^2.0.0",
|
||||||
|
"responselike": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-cache-semantics": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/http2-wrapper": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"quick-lru": "^5.1.1",
|
||||||
|
"resolve-alpn": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-buffer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/json-stringify-safe": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/keyv": {
|
||||||
|
"version": "4.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-buffer": "3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lowercase-keys": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/matcher": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/normalize-url": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-cancelable": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/progress": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quick-lru": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-alpn": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/responselike": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lowercase-keys": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/roarr": {
|
||||||
|
"version": "2.15.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||||
|
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"boolean": "^3.0.1",
|
||||||
|
"detect-node": "^2.0.4",
|
||||||
|
"globalthis": "^1.0.1",
|
||||||
|
"json-stringify-safe": "^5.0.1",
|
||||||
|
"semver-compare": "^1.0.0",
|
||||||
|
"sprintf-js": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver-compare": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/serialize-error": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"type-fest": "^0.13.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sprintf-js": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/sql.js": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/sumchecker": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "0.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||||
|
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "2fa-account-tool",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "本地2FA账号密码管理工具",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron ."
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"2fa",
|
||||||
|
"password-manager",
|
||||||
|
"totp"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^28.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"sql.js": "^1.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
preload.js
Normal file
21
preload.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('api', {
|
||||||
|
// Vault operations
|
||||||
|
getVaults: () => ipcRenderer.invoke('db:getVaults'),
|
||||||
|
addVault: (vault) => ipcRenderer.invoke('db:addVault', vault),
|
||||||
|
updateVault: (id, vault) => ipcRenderer.invoke('db:updateVault', id, vault),
|
||||||
|
deleteVault: (id) => ipcRenderer.invoke('db:deleteVault', id),
|
||||||
|
|
||||||
|
// Account operations
|
||||||
|
getAccounts: (page, limit, vaultId) => ipcRenderer.invoke('db:getAccounts', page, limit, vaultId),
|
||||||
|
getAccountCount: (vaultId) => ipcRenderer.invoke('db:getAccountCount', vaultId),
|
||||||
|
addAccount: (account) => ipcRenderer.invoke('db:addAccount', account),
|
||||||
|
updateAccount: (id, account) => ipcRenderer.invoke('db:updateAccount', id, account),
|
||||||
|
deleteAccount: (id) => ipcRenderer.invoke('db:deleteAccount', id),
|
||||||
|
moveAccountToVault: (accountId, vaultId) => ipcRenderer.invoke('db:moveAccountToVault', accountId, vaultId),
|
||||||
|
searchAccounts: (query, vaultId) => ipcRenderer.invoke('db:searchAccounts', query, vaultId),
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
copyToClipboard: (text) => ipcRenderer.invoke('clipboard:write', text)
|
||||||
|
});
|
||||||
322
src/db/database.js
Normal file
322
src/db/database.js
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
const initSqlJs = require('sql.js');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { app } = require('electron');
|
||||||
|
const CryptoJS = require('crypto-js');
|
||||||
|
|
||||||
|
const ENCRYPTION_KEY = 'your-secret-key-2fa-manager-v1';
|
||||||
|
|
||||||
|
class AccountDatabase {
|
||||||
|
constructor() {
|
||||||
|
this.db = null;
|
||||||
|
this.dbPath = null;
|
||||||
|
this.ready = this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const SQL = await initSqlJs();
|
||||||
|
|
||||||
|
// const userDataPath = app.getPath('userData');
|
||||||
|
// this.dbPath = path.join(userDataPath, 'accounts.db');
|
||||||
|
|
||||||
|
// 便携版:将数据库放在项目根目录下
|
||||||
|
this.dbPath = path.join(process.cwd(), 'accounts.db');
|
||||||
|
|
||||||
|
// 尝试加载现有数据库
|
||||||
|
if (fs.existsSync(this.dbPath)) {
|
||||||
|
const buffer = fs.readFileSync(this.dbPath);
|
||||||
|
this.db = new SQL.Database(buffer);
|
||||||
|
} else {
|
||||||
|
this.db = new SQL.Database();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建资料库表
|
||||||
|
this.db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS vaults (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
icon TEXT DEFAULT '📁',
|
||||||
|
color TEXT DEFAULT '#3b82f6',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 创建账号表
|
||||||
|
this.db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
vault_id INTEGER DEFAULT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
username TEXT,
|
||||||
|
password TEXT,
|
||||||
|
totp_secret TEXT,
|
||||||
|
tags TEXT,
|
||||||
|
email TEXT,
|
||||||
|
proxy TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (vault_id) REFERENCES vaults(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 迁移: 添加 tags 和 vault_id 和 proxy 列 (如果不存在)
|
||||||
|
try { this.db.run(`ALTER TABLE accounts ADD COLUMN tags TEXT`); } catch (e) { }
|
||||||
|
try { this.db.run(`ALTER TABLE accounts ADD COLUMN vault_id INTEGER`); } catch (e) { }
|
||||||
|
try { this.db.run(`ALTER TABLE accounts ADD COLUMN proxy TEXT`); } catch (e) { }
|
||||||
|
|
||||||
|
// 创建默认资料库 (如果不存在)
|
||||||
|
const defaultVault = this.db.exec("SELECT id FROM vaults WHERE name = '默认'");
|
||||||
|
if (!defaultVault.length || !defaultVault[0].values.length) {
|
||||||
|
this.db.run("INSERT INTO vaults (name, icon) VALUES ('默认', '🏠')");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建索引
|
||||||
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_name ON accounts(name)`);
|
||||||
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username)`);
|
||||||
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_vault ON accounts(vault_id)`);
|
||||||
|
|
||||||
|
this.save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if (!this.db || !this.dbPath) return;
|
||||||
|
const data = this.db.export();
|
||||||
|
const buffer = Buffer.from(data);
|
||||||
|
fs.writeFileSync(this.dbPath, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
return CryptoJS.AES.encrypt(text, ENCRYPTION_KEY).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(ciphertext) {
|
||||||
|
if (!ciphertext) return '';
|
||||||
|
try {
|
||||||
|
const bytes = CryptoJS.AES.decrypt(ciphertext, ENCRYPTION_KEY);
|
||||||
|
return bytes.toString(CryptoJS.enc.Utf8);
|
||||||
|
} catch (e) {
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 资料库操作 ====================
|
||||||
|
|
||||||
|
async getVaults() {
|
||||||
|
await this.ready;
|
||||||
|
const result = this.db.exec(`
|
||||||
|
SELECT v.*, COUNT(a.id) as account_count
|
||||||
|
FROM vaults v
|
||||||
|
LEFT JOIN accounts a ON a.vault_id = v.id
|
||||||
|
GROUP BY v.id
|
||||||
|
ORDER BY v.created_at ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!result.length) return [];
|
||||||
|
|
||||||
|
const columns = result[0].columns;
|
||||||
|
return result[0].values.map(row => {
|
||||||
|
const obj = {};
|
||||||
|
columns.forEach((col, i) => obj[col] = row[i]);
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addVault(vault) {
|
||||||
|
await this.ready;
|
||||||
|
this.db.run(`INSERT INTO vaults (name, icon, color) VALUES (?, ?, ?)`, [
|
||||||
|
vault.name,
|
||||||
|
vault.icon || '📁',
|
||||||
|
vault.color || '#3b82f6'
|
||||||
|
]);
|
||||||
|
this.save();
|
||||||
|
|
||||||
|
const result = this.db.exec('SELECT last_insert_rowid() as id');
|
||||||
|
return { id: result[0]?.values[0][0], ...vault };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateVault(id, vault) {
|
||||||
|
await this.ready;
|
||||||
|
this.db.run(`UPDATE vaults SET name = ?, icon = ?, color = ? WHERE id = ?`, [
|
||||||
|
vault.name,
|
||||||
|
vault.icon || '📁',
|
||||||
|
vault.color || '#3b82f6',
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
this.save();
|
||||||
|
return { id, ...vault };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteVault(id) {
|
||||||
|
await this.ready;
|
||||||
|
// 将该资料库中的账号移到默认资料库
|
||||||
|
const defaultVault = this.db.exec("SELECT id FROM vaults WHERE name = '默认'");
|
||||||
|
const defaultId = defaultVault[0]?.values[0]?.[0] || null;
|
||||||
|
|
||||||
|
this.db.run('UPDATE accounts SET vault_id = ? WHERE vault_id = ?', [defaultId, id]);
|
||||||
|
this.db.run('DELETE FROM vaults WHERE id = ? AND name != ?', [id, '默认']);
|
||||||
|
this.save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 账号操作 ====================
|
||||||
|
|
||||||
|
async getAccounts(page = 1, limit = 10, vaultId = null) {
|
||||||
|
await this.ready;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
let sql = `SELECT * FROM accounts`;
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (vaultId !== null) {
|
||||||
|
sql += ` WHERE vault_id = ?`;
|
||||||
|
params.push(vaultId);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ` ORDER BY updated_at DESC LIMIT ? OFFSET ?`;
|
||||||
|
params.push(limit, offset);
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(sql);
|
||||||
|
stmt.bind(params);
|
||||||
|
|
||||||
|
const accounts = [];
|
||||||
|
while (stmt.step()) {
|
||||||
|
const row = stmt.getAsObject();
|
||||||
|
accounts.push({
|
||||||
|
...row,
|
||||||
|
password: this.decrypt(row.password),
|
||||||
|
totp_secret: this.decrypt(row.totp_secret)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stmt.free();
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccountCount(vaultId = null) {
|
||||||
|
await this.ready;
|
||||||
|
let sql = 'SELECT COUNT(*) as count FROM accounts';
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (vaultId !== null) {
|
||||||
|
sql += ' WHERE vault_id = ?';
|
||||||
|
params.push(vaultId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(sql);
|
||||||
|
stmt.bind(params);
|
||||||
|
stmt.step();
|
||||||
|
const result = stmt.getAsObject();
|
||||||
|
stmt.free();
|
||||||
|
|
||||||
|
return result.count || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAccount(account) {
|
||||||
|
await this.ready;
|
||||||
|
|
||||||
|
this.db.run(`
|
||||||
|
INSERT INTO accounts (vault_id, name, username, password, totp_secret, tags, email, proxy, notes)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, [
|
||||||
|
account.vault_id || null,
|
||||||
|
account.name,
|
||||||
|
account.username,
|
||||||
|
this.encrypt(account.password),
|
||||||
|
this.encrypt(account.totp_secret),
|
||||||
|
account.tags || '',
|
||||||
|
account.email,
|
||||||
|
account.proxy || '',
|
||||||
|
account.notes
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.save();
|
||||||
|
|
||||||
|
const result = this.db.exec('SELECT last_insert_rowid() as id');
|
||||||
|
const id = result[0]?.values[0][0];
|
||||||
|
|
||||||
|
return { id, ...account };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAccount(id, account) {
|
||||||
|
await this.ready;
|
||||||
|
|
||||||
|
this.db.run(`
|
||||||
|
UPDATE accounts
|
||||||
|
SET vault_id = ?, name = ?, username = ?, password = ?, totp_secret = ?,
|
||||||
|
tags = ?, email = ?, proxy = ?, notes = ?, updated_at = datetime('now')
|
||||||
|
WHERE id = ?
|
||||||
|
`, [
|
||||||
|
account.vault_id || null,
|
||||||
|
account.name,
|
||||||
|
account.username,
|
||||||
|
this.encrypt(account.password),
|
||||||
|
this.encrypt(account.totp_secret),
|
||||||
|
account.tags || '',
|
||||||
|
account.email,
|
||||||
|
account.proxy || '',
|
||||||
|
account.notes,
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.save();
|
||||||
|
return { id, ...account };
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveAccountToVault(accountId, vaultId) {
|
||||||
|
await this.ready;
|
||||||
|
this.db.run('UPDATE accounts SET vault_id = ?, updated_at = datetime("now") WHERE id = ?',
|
||||||
|
[vaultId, accountId]);
|
||||||
|
this.save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAccount(id) {
|
||||||
|
await this.ready;
|
||||||
|
this.db.run('DELETE FROM accounts WHERE id = ?', [id]);
|
||||||
|
this.save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchAccounts(query, vaultId = null) {
|
||||||
|
await this.ready;
|
||||||
|
const searchTerm = `%${query}%`;
|
||||||
|
|
||||||
|
let sql = `SELECT * FROM accounts WHERE (name LIKE ? OR username LIKE ? OR email LIKE ? OR tags LIKE ? OR proxy LIKE ?)`;
|
||||||
|
const params = [searchTerm, searchTerm, searchTerm, searchTerm, searchTerm];
|
||||||
|
|
||||||
|
if (vaultId !== null) {
|
||||||
|
sql += ` AND vault_id = ?`;
|
||||||
|
params.push(vaultId);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ` ORDER BY updated_at DESC LIMIT 50`;
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(sql);
|
||||||
|
stmt.bind(params);
|
||||||
|
|
||||||
|
const accounts = [];
|
||||||
|
while (stmt.step()) {
|
||||||
|
const row = stmt.getAsObject();
|
||||||
|
accounts.push({
|
||||||
|
...row,
|
||||||
|
password: this.decrypt(row.password),
|
||||||
|
totp_secret: this.decrypt(row.totp_secret)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stmt.free();
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.db) {
|
||||||
|
this.save();
|
||||||
|
this.db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AccountDatabase;
|
||||||
198
src/index.html
Normal file
198
src/index.html
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com">
|
||||||
|
<title>2FA 账号管理器</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 左侧区域 -->
|
||||||
|
<div class="list-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h1><i class="fa-solid fa-shield-halved"></i> 账号列表</h1>
|
||||||
|
<div class="search-box">
|
||||||
|
<i class="fa-solid fa-magnifying-glass search-icon"></i>
|
||||||
|
<input type="text" id="searchInput" placeholder="搜索账号..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域:左边资料库 + 右边账号卡片 -->
|
||||||
|
<div class="content-area">
|
||||||
|
<!-- 资料库侧边栏 -->
|
||||||
|
<div class="vault-sidebar">
|
||||||
|
<div class="vault-sidebar-header">
|
||||||
|
<span>资料库</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vault-items">
|
||||||
|
<div class="vault-row all-vault active" onclick="selectVault(null)">
|
||||||
|
<span class="vault-name">全部</span>
|
||||||
|
<span class="vault-count" id="allCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div id="vaultList">
|
||||||
|
<!-- 动态生成资料库列表 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="vault-add-btn" onclick="showAddVaultForm()">
|
||||||
|
<span>+ 新增</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 账号卡片区域 -->
|
||||||
|
<div class="accounts-list" id="accountsList">
|
||||||
|
<!-- 动态生成账号卡片 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination" id="pagination">
|
||||||
|
<button id="prevBtn" class="page-btn" disabled><i class="fa-solid fa-chevron-left"></i> 上一页</button>
|
||||||
|
<span class="page-info" id="pageInfo">1 / 1</span>
|
||||||
|
<button id="nextBtn" class="page-btn">下一页 <i class="fa-solid fa-chevron-right"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:表单区域 -->
|
||||||
|
<div class="form-panel">
|
||||||
|
<!-- 新增资料库表单 (隐藏) -->
|
||||||
|
<div class="vault-form-section" id="vaultFormSection" style="display: none;">
|
||||||
|
<h3><i class="fa-solid fa-folder-plus"></i> <span id="vaultFormTitle">新增资料库</span></h3>
|
||||||
|
<input type="hidden" id="vaultId" value="">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vaultName">资料库名称</label>
|
||||||
|
<input type="text" id="vaultName" placeholder="如: 工作账号、游戏账号" maxlength="20">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" id="cancelVaultBtn" class="btn-secondary">取消</button>
|
||||||
|
<button type="button" id="saveVaultBtn" class="btn-primary">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h2 id="formTitle"><i class="fa-solid fa-user-plus"></i> 添加账号</h2>
|
||||||
|
<form id="accountForm">
|
||||||
|
<input type="hidden" id="accountId" value="">
|
||||||
|
<input type="hidden" id="accountVaultId" value="">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">用户名</label>
|
||||||
|
<input type="text" id="username" placeholder="登录用户名">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input type="text" id="password" placeholder="登录密码">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="totpSecret">2FA 密钥</label>
|
||||||
|
<input type="text" id="totpSecret" placeholder="TOTP Secret Key">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">关联邮箱</label>
|
||||||
|
<input type="email" id="email" placeholder="关联邮箱">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="proxy">代理</label>
|
||||||
|
<input type="text" id="proxy" placeholder="如: socks5://127.0.0.1:1080">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tags">标签</label>
|
||||||
|
<input type="text" id="tags" placeholder="用逗号分隔,如: 工作, 社交">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notes">备注</label>
|
||||||
|
<textarea id="notes" rows="2" placeholder="其他备忘信息..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" id="cancelBtn" class="btn-secondary">取消</button>
|
||||||
|
<button type="submit" class="btn-primary"><i class="fa-solid fa-floppy-disk"></i> 保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 随机生成器 -->
|
||||||
|
<div class="generator-section">
|
||||||
|
<h3><i class="fa-solid fa-wand-magic-sparkles"></i> 随机生成器</h3>
|
||||||
|
|
||||||
|
<div class="generator-item">
|
||||||
|
<label>随机姓名</label>
|
||||||
|
<div class="generator-row">
|
||||||
|
<input type="text" id="randomFullName" readonly placeholder="First Last">
|
||||||
|
<button type="button" id="rollName" class="btn-icon" title="重新生成"><i class="fa-solid fa-dice"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="generator-item">
|
||||||
|
<label>用户名 (基于姓名)</label>
|
||||||
|
<div class="generator-row">
|
||||||
|
<input type="text" id="randomUsername" readonly>
|
||||||
|
<button type="button" class="btn-icon copy-btn" data-target="randomUsername" title="复制"><i
|
||||||
|
class="fa-solid fa-copy"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="generator-item">
|
||||||
|
<label>随机密码</label>
|
||||||
|
<div class="password-options">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="pwdUppercase" checked> A-Z
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="pwdLowercase" checked> a-z
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="pwdNumbers" checked> 0-9
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="pwdSymbols" checked> !@#
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
长度: <input type="number" id="pwdLength" value="16" min="8" max="64" style="width: 50px;">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="generator-row">
|
||||||
|
<input type="text" id="randomPassword" readonly>
|
||||||
|
<button type="button" id="rollPassword" class="btn-icon" title="重新生成"><i
|
||||||
|
class="fa-solid fa-dice"></i></button>
|
||||||
|
<button type="button" class="btn-icon copy-btn" data-target="randomPassword" title="复制"><i
|
||||||
|
class="fa-solid fa-copy"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 迁移对话框 -->
|
||||||
|
<div class="modal" id="moveModal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3><i class="fa-solid fa-truck-arrow-right"></i> 迁移账号到资料库</h3>
|
||||||
|
<select id="moveToVault"></select>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" id="cancelMoveBtn" class="btn-secondary">取消</button>
|
||||||
|
<button type="button" id="confirmMoveBtn" class="btn-primary">确认迁移</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
|
<script src="renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
813
src/renderer.js
Normal file
813
src/renderer.js
Normal file
@@ -0,0 +1,813 @@
|
|||||||
|
// TOTP 生成器
|
||||||
|
class TOTP {
|
||||||
|
static async generate(secret) {
|
||||||
|
if (!secret) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
let bits = '';
|
||||||
|
const cleanSecret = secret.replace(/\s/g, '').toUpperCase();
|
||||||
|
|
||||||
|
for (let i = 0; i < cleanSecret.length; i++) {
|
||||||
|
const val = base32chars.indexOf(cleanSecret[i]);
|
||||||
|
if (val === -1) continue;
|
||||||
|
bits += val.toString(2).padStart(5, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(Math.floor(bits.length / 8));
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
bytes[i] = parseInt(bits.substr(i * 8, 8), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const epoch = Math.floor(Date.now() / 1000);
|
||||||
|
const timeStep = Math.floor(epoch / 30);
|
||||||
|
|
||||||
|
const timeBuffer = new ArrayBuffer(8);
|
||||||
|
const timeView = new DataView(timeBuffer);
|
||||||
|
timeView.setUint32(4, timeStep, false);
|
||||||
|
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
'raw', bytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']
|
||||||
|
);
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign('HMAC', key, timeBuffer);
|
||||||
|
const hmac = new Uint8Array(signature);
|
||||||
|
|
||||||
|
const offset = hmac[hmac.length - 1] & 0xf;
|
||||||
|
const binary = ((hmac[offset] & 0x7f) << 24) |
|
||||||
|
((hmac[offset + 1] & 0xff) << 16) |
|
||||||
|
((hmac[offset + 2] & 0xff) << 8) |
|
||||||
|
(hmac[offset + 3] & 0xff);
|
||||||
|
|
||||||
|
const otp = binary % 1000000;
|
||||||
|
return otp.toString().padStart(6, '0');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('TOTP generation error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTimeRemaining() {
|
||||||
|
return 30 - (Math.floor(Date.now() / 1000) % 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机名称库
|
||||||
|
const firstNames = [
|
||||||
|
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph',
|
||||||
|
'Thomas', 'Christopher', 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth',
|
||||||
|
'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen', 'Alex', 'Jordan', 'Taylor',
|
||||||
|
'Morgan', 'Casey', 'Riley', 'Quinn', 'Avery', 'Peyton', 'Cameron', 'Emma',
|
||||||
|
'Oliver', 'Liam', 'Noah', 'Sophia', 'Ava', 'Isabella', 'Mia', 'Charlotte',
|
||||||
|
'Benjamin', 'Alexander', 'Sebastian', 'Theodore', 'Victoria', 'Penelope'
|
||||||
|
];
|
||||||
|
|
||||||
|
const lastNames = [
|
||||||
|
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',
|
||||||
|
'Rodriguez', 'Martinez', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson',
|
||||||
|
'Martin', 'Lee', 'Thompson', 'White', 'Harris', 'Clark', 'Lewis', 'Robinson',
|
||||||
|
'Walker', 'Young', 'King', 'Wright', 'Scott', 'Green', 'Baker', 'Adams',
|
||||||
|
'Nelson', 'Mitchell', 'Campbell', 'Roberts', 'Carter', 'Phillips', 'Evans'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 应用状态
|
||||||
|
let accounts = [];
|
||||||
|
let vaults = [];
|
||||||
|
let currentPage = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
const itemsPerPage = 8;
|
||||||
|
let editingId = null;
|
||||||
|
let selectedVaultId = null;
|
||||||
|
let movingAccountId = null;
|
||||||
|
let totalAccountCount = 0;
|
||||||
|
|
||||||
|
// DOM 元素
|
||||||
|
const accountsList = document.getElementById('accountsList');
|
||||||
|
const accountForm = document.getElementById('accountForm');
|
||||||
|
const formTitle = document.getElementById('formTitle');
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const prevBtn = document.getElementById('prevBtn');
|
||||||
|
const nextBtn = document.getElementById('nextBtn');
|
||||||
|
const pageInfo = document.getElementById('pageInfo');
|
||||||
|
const cancelBtn = document.getElementById('cancelBtn');
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
const vaultList = document.getElementById('vaultList');
|
||||||
|
const vaultFormSection = document.getElementById('vaultFormSection');
|
||||||
|
const moveModal = document.getElementById('moveModal');
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadVaults();
|
||||||
|
loadAccounts();
|
||||||
|
generateRandomName();
|
||||||
|
generateRandomPassword();
|
||||||
|
setupEventListeners();
|
||||||
|
setInterval(updateAllTOTP, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupEventListeners() {
|
||||||
|
accountForm.addEventListener('submit', handleFormSubmit);
|
||||||
|
cancelBtn.addEventListener('click', resetForm);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
let searchTimeout;
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
if (e.target.value.trim()) {
|
||||||
|
searchAccounts(e.target.value.trim());
|
||||||
|
} else {
|
||||||
|
loadAccounts();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
prevBtn.addEventListener('click', () => {
|
||||||
|
if (currentPage > 1) { currentPage--; loadAccounts(); }
|
||||||
|
});
|
||||||
|
nextBtn.addEventListener('click', () => {
|
||||||
|
if (currentPage < totalPages) { currentPage++; loadAccounts(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 随机生成器
|
||||||
|
document.getElementById('rollName').addEventListener('click', generateRandomName);
|
||||||
|
document.getElementById('rollPassword').addEventListener('click', generateRandomPassword);
|
||||||
|
|
||||||
|
document.querySelectorAll('.generator-section .copy-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const targetId = btn.dataset.target;
|
||||||
|
const value = document.getElementById(targetId).value;
|
||||||
|
copyToClipboard(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('randomFullName').addEventListener('click', function () {
|
||||||
|
copyToClipboard(this.value);
|
||||||
|
});
|
||||||
|
document.getElementById('randomUsername').addEventListener('click', function () {
|
||||||
|
copyToClipboard(this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
['pwdUppercase', 'pwdLowercase', 'pwdNumbers', 'pwdSymbols', 'pwdLength'].forEach(id => {
|
||||||
|
document.getElementById(id).addEventListener('change', generateRandomPassword);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 资料库表单 (保留用于兼容)
|
||||||
|
document.getElementById('cancelVaultBtn').addEventListener('click', () => {
|
||||||
|
vaultFormSection.style.display = 'none';
|
||||||
|
});
|
||||||
|
document.getElementById('saveVaultBtn').addEventListener('click', saveVault);
|
||||||
|
|
||||||
|
// 迁移模态框
|
||||||
|
document.getElementById('cancelMoveBtn').addEventListener('click', () => {
|
||||||
|
moveModal.style.display = 'none';
|
||||||
|
movingAccountId = null;
|
||||||
|
});
|
||||||
|
document.getElementById('confirmMoveBtn').addEventListener('click', confirmMoveAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 资料库操作 ====================
|
||||||
|
|
||||||
|
async function loadVaults() {
|
||||||
|
try {
|
||||||
|
vaults = await window.api.getVaults();
|
||||||
|
totalAccountCount = await window.api.getAccountCount(null);
|
||||||
|
renderVaultList();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load vaults:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVaultList() {
|
||||||
|
// 更新全部账号数量
|
||||||
|
document.getElementById('allCount').textContent = totalAccountCount;
|
||||||
|
|
||||||
|
// 更新全部账号选中状态
|
||||||
|
const allVaultRow = document.querySelector('.vault-row.all-vault');
|
||||||
|
if (allVaultRow) {
|
||||||
|
allVaultRow.classList.toggle('active', selectedVaultId === null);
|
||||||
|
}
|
||||||
|
|
||||||
|
vaultList.innerHTML = vaults.map(v => `
|
||||||
|
<div class="vault-row ${selectedVaultId === v.id ? 'active' : ''}"
|
||||||
|
data-id="${v.id}"
|
||||||
|
data-name="${escapeHtml(v.name)}">
|
||||||
|
<span class="vault-name">${escapeHtml(v.name)}</span>
|
||||||
|
<span class="vault-count">${v.account_count || 0}</span>
|
||||||
|
${v.name !== '默认' ? `
|
||||||
|
<button class="vault-delete" title="删除">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
vaultList.querySelectorAll('.vault-row').forEach(row => {
|
||||||
|
const id = parseInt(row.dataset.id);
|
||||||
|
const vault = vaults.find(v => v.id === id);
|
||||||
|
if (!vault) return;
|
||||||
|
|
||||||
|
let clickTimeout = null;
|
||||||
|
|
||||||
|
row.addEventListener('click', (e) => {
|
||||||
|
if (row.classList.contains('editing')) return;
|
||||||
|
if (e.target.closest('.vault-delete')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteVault(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟执行单击,以便检测是否是双击
|
||||||
|
if (clickTimeout) {
|
||||||
|
clearTimeout(clickTimeout);
|
||||||
|
clickTimeout = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clickTimeout = setTimeout(() => {
|
||||||
|
clickTimeout = null;
|
||||||
|
selectVault(id);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只有非默认资料库可以双击改名
|
||||||
|
if (vault.name !== '默认') {
|
||||||
|
row.addEventListener('dblclick', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
// 取消单击
|
||||||
|
if (clickTimeout) {
|
||||||
|
clearTimeout(clickTimeout);
|
||||||
|
clickTimeout = null;
|
||||||
|
}
|
||||||
|
startEditVault(row, id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectVault(id) {
|
||||||
|
selectedVaultId = id;
|
||||||
|
currentPage = 1;
|
||||||
|
loadAccounts();
|
||||||
|
renderVaultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双击资料库开始编辑
|
||||||
|
function startEditVault(row, id) {
|
||||||
|
const vault = vaults.find(v => v.id === id);
|
||||||
|
if (!vault || vault.name === '默认') return;
|
||||||
|
|
||||||
|
row.classList.add('editing');
|
||||||
|
const nameSpan = row.querySelector('.vault-name');
|
||||||
|
const originalName = vault.name;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.className = 'vault-edit';
|
||||||
|
input.value = originalName;
|
||||||
|
nameSpan.replaceWith(input);
|
||||||
|
|
||||||
|
// 延迟聚焦以确保元素已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
let isFinished = false;
|
||||||
|
async function finishEdit() {
|
||||||
|
if (isFinished) return;
|
||||||
|
isFinished = true;
|
||||||
|
|
||||||
|
const newName = input.value.trim();
|
||||||
|
if (newName && newName !== originalName) {
|
||||||
|
try {
|
||||||
|
await window.api.updateVault(id, { name: newName });
|
||||||
|
showToast('资料库已更新', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Update vault failed:', e);
|
||||||
|
showToast('更新失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadVaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('blur', finishEdit);
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
input.blur();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
input.value = originalName;
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 阻止输入框的点击事件冒泡
|
||||||
|
input.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击 + 新增资料库 (行内创建)
|
||||||
|
async function showAddVaultForm() {
|
||||||
|
// 检查是否已有输入框
|
||||||
|
if (vaultList.querySelector('.vault-new-row')) return;
|
||||||
|
|
||||||
|
// 创建新增行
|
||||||
|
const editRow = document.createElement('div');
|
||||||
|
editRow.className = 'vault-new-row';
|
||||||
|
editRow.innerHTML = `
|
||||||
|
<input type="text" placeholder="输入名称..." />
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 插入到 vaultList 末尾
|
||||||
|
vaultList.appendChild(editRow);
|
||||||
|
|
||||||
|
const input = editRow.querySelector('input');
|
||||||
|
|
||||||
|
// 延迟聚焦
|
||||||
|
setTimeout(() => {
|
||||||
|
input.focus();
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
let isFinished = false;
|
||||||
|
async function finishCreate() {
|
||||||
|
if (isFinished) return;
|
||||||
|
isFinished = true;
|
||||||
|
|
||||||
|
const name = input.value.trim();
|
||||||
|
if (name) {
|
||||||
|
try {
|
||||||
|
await window.api.addVault({ name });
|
||||||
|
showToast('资料库已创建', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Add vault failed:', e);
|
||||||
|
showToast('创建失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadVaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('blur', finishCreate);
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
input.blur();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
loadVaults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 阻止点击事件冒泡
|
||||||
|
input.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveVault() {
|
||||||
|
const id = document.getElementById('vaultId').value;
|
||||||
|
const name = document.getElementById('vaultName').value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
showToast('请输入资料库名称', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (id) {
|
||||||
|
await window.api.updateVault(parseInt(id), { name });
|
||||||
|
showToast('资料库已更新', 'success');
|
||||||
|
} else {
|
||||||
|
await window.api.addVault({ name });
|
||||||
|
showToast('资料库已创建', 'success');
|
||||||
|
}
|
||||||
|
vaultFormSection.style.display = 'none';
|
||||||
|
loadVaults();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Save vault failed:', e);
|
||||||
|
showToast('保存失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteVault(id) {
|
||||||
|
const vault = vaults.find(v => v.id === id);
|
||||||
|
if (!vault || vault.name === '默认') return;
|
||||||
|
|
||||||
|
if (!confirm(`确定要删除资料库「${vault.name}」吗?\n其中的账号将被移到默认资料库。`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.api.deleteVault(id);
|
||||||
|
showToast('资料库已删除', 'success');
|
||||||
|
if (selectedVaultId === id) {
|
||||||
|
selectedVaultId = null;
|
||||||
|
}
|
||||||
|
loadVaults();
|
||||||
|
loadAccounts();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Delete vault failed:', e);
|
||||||
|
showToast('删除失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMoveDialog(accountId) {
|
||||||
|
movingAccountId = accountId;
|
||||||
|
const select = document.getElementById('moveToVault');
|
||||||
|
select.innerHTML = vaults.map(v =>
|
||||||
|
`<option value="${v.id}">${escapeHtml(v.name)}</option>`
|
||||||
|
).join('');
|
||||||
|
moveModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmMoveAccount() {
|
||||||
|
if (!movingAccountId) return;
|
||||||
|
|
||||||
|
const newVaultId = parseInt(document.getElementById('moveToVault').value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.api.moveAccountToVault(movingAccountId, newVaultId);
|
||||||
|
showToast('账号已迁移', 'success');
|
||||||
|
moveModal.style.display = 'none';
|
||||||
|
movingAccountId = null;
|
||||||
|
loadAccounts();
|
||||||
|
loadVaults();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Move failed:', e);
|
||||||
|
showToast('迁移失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 账号操作 ====================
|
||||||
|
|
||||||
|
async function loadAccounts() {
|
||||||
|
try {
|
||||||
|
const count = await window.api.getAccountCount(selectedVaultId);
|
||||||
|
totalPages = Math.max(1, Math.ceil(count / itemsPerPage));
|
||||||
|
|
||||||
|
if (currentPage > totalPages) {
|
||||||
|
currentPage = totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts = await window.api.getAccounts(currentPage, itemsPerPage, selectedVaultId);
|
||||||
|
renderAccounts();
|
||||||
|
updatePagination();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load accounts:', e);
|
||||||
|
showToast('加载账号失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchAccounts(query) {
|
||||||
|
try {
|
||||||
|
accounts = await window.api.searchAccounts(query, selectedVaultId);
|
||||||
|
currentPage = 1;
|
||||||
|
totalPages = 1;
|
||||||
|
renderAccounts();
|
||||||
|
updatePagination();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Search failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAccounts() {
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
accountsList.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon"><i class="fa-solid fa-inbox"></i></div>
|
||||||
|
<div class="empty-state-text">暂无账号<br>在右侧添加你的第一个账号</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
accountsList.innerHTML = accounts.map(acc => {
|
||||||
|
const tags = acc.tags ? acc.tags.split(',').map(t => t.trim()).filter(t => t) : [];
|
||||||
|
const tagsHtml = tags.length > 0
|
||||||
|
? `<div class="card-tags">${tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="account-card" data-id="${acc.id}">
|
||||||
|
<div class="card-header">
|
||||||
|
${tagsHtml || '<span class="card-name-placeholder">无标签</span>'}
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="card-action-btn move" onclick="openMoveDialog(${acc.id})" title="迁移">
|
||||||
|
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
||||||
|
</button>
|
||||||
|
<button class="card-action-btn edit" onclick="editAccount(${acc.id})" title="编辑">
|
||||||
|
<i class="fa-solid fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
<button class="card-action-btn delete" onclick="deleteAccount(${acc.id})" title="删除">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${acc.username ? `
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">账号</span>
|
||||||
|
<span class="info-value" onclick="copyValue(this, '${escapeJs(acc.username)}')">${escapeHtml(acc.username)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${acc.password ? `
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">密码</span>
|
||||||
|
<span class="info-value password" onclick="copyValue(this, '${escapeJs(acc.password)}')">••••••••</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${acc.email ? `
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">邮箱</span>
|
||||||
|
<span class="info-value" onclick="copyValue(this, '${escapeJs(acc.email)}')">${escapeHtml(acc.email)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${acc.proxy ? `
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">代理</span>
|
||||||
|
<span class="info-value" onclick="copyValue(this, '${escapeJs(acc.proxy)}')">${escapeHtml(acc.proxy)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${acc.totp_secret ? `
|
||||||
|
<div class="totp-section">
|
||||||
|
<div class="totp-display totp-hidden" id="totp-container-${acc.id}" onclick="showTOTP(${acc.id}, '${escapeJs(acc.totp_secret)}')">
|
||||||
|
<span class="totp-placeholder"><i class="fa-solid fa-arrow-pointer"></i> 点击显示 2FA</span>
|
||||||
|
</div>
|
||||||
|
<div class="totp-display totp-visible" id="totp-visible-${acc.id}" style="display: none;">
|
||||||
|
<span class="totp-code" id="totp-${acc.id}" onclick="copyTOTP(${acc.id})">------</span>
|
||||||
|
<div class="totp-timer" id="timer-${acc.id}">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 32 32">
|
||||||
|
<circle class="bg" cx="16" cy="16" r="14"></circle>
|
||||||
|
<circle class="progress" cx="16" cy="16" r="14"
|
||||||
|
stroke-dasharray="88" stroke-dashoffset="0"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// 不再自动更新TOTP,改为点击时才显示
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存已激活的TOTP账号列表
|
||||||
|
const activeTOTPs = new Map();
|
||||||
|
|
||||||
|
// 显示TOTP
|
||||||
|
async function showTOTP(id, secret) {
|
||||||
|
const container = document.getElementById(`totp-container-${id}`);
|
||||||
|
const visible = document.getElementById(`totp-visible-${id}`);
|
||||||
|
|
||||||
|
if (!container || !visible) return;
|
||||||
|
|
||||||
|
container.style.display = 'none';
|
||||||
|
visible.style.display = 'flex';
|
||||||
|
|
||||||
|
// 保存到激活列表
|
||||||
|
activeTOTPs.set(id, secret);
|
||||||
|
|
||||||
|
// 立即更新一次
|
||||||
|
await updateTOTP(id, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyValue(element, value) {
|
||||||
|
try {
|
||||||
|
await window.api.copyToClipboard(value);
|
||||||
|
element.classList.add('copied');
|
||||||
|
showToast('已复制', 'success');
|
||||||
|
setTimeout(() => { element.classList.remove('copied'); }, 300);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Copy failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTOTP(id, secret) {
|
||||||
|
const codeEl = document.getElementById(`totp-${id}`);
|
||||||
|
const timerEl = document.getElementById(`timer-${id}`);
|
||||||
|
|
||||||
|
if (!codeEl || !timerEl) return;
|
||||||
|
|
||||||
|
const code = await TOTP.generate(secret);
|
||||||
|
if (code) {
|
||||||
|
codeEl.textContent = code.substring(0, 3) + ' ' + code.substring(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = TOTP.getTimeRemaining();
|
||||||
|
const progress = (remaining / 30) * 88;
|
||||||
|
const progressCircle = timerEl.querySelector('.progress');
|
||||||
|
progressCircle.style.strokeDashoffset = 88 - progress;
|
||||||
|
|
||||||
|
timerEl.classList.remove('warning', 'danger');
|
||||||
|
if (remaining <= 5) {
|
||||||
|
timerEl.classList.add('danger');
|
||||||
|
} else if (remaining <= 10) {
|
||||||
|
timerEl.classList.add('warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAllTOTP() {
|
||||||
|
// 只更新已激活显示的TOTP
|
||||||
|
activeTOTPs.forEach((secret, id) => {
|
||||||
|
updateTOTP(id, secret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTOTP(id) {
|
||||||
|
const acc = accounts.find(a => a.id === id);
|
||||||
|
if (!acc || !acc.totp_secret) return;
|
||||||
|
|
||||||
|
const code = await TOTP.generate(acc.totp_secret);
|
||||||
|
if (code) {
|
||||||
|
const codeEl = document.getElementById(`totp-${id}`);
|
||||||
|
codeEl.classList.add('copied');
|
||||||
|
await window.api.copyToClipboard(code);
|
||||||
|
showToast('已复制', 'success');
|
||||||
|
setTimeout(() => { codeEl.classList.remove('copied'); }, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagination() {
|
||||||
|
pageInfo.textContent = `${currentPage} / ${totalPages}`;
|
||||||
|
prevBtn.disabled = currentPage <= 1;
|
||||||
|
nextBtn.disabled = currentPage >= totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value.trim();
|
||||||
|
const tags = document.getElementById('tags').value.trim();
|
||||||
|
|
||||||
|
// 使用标签的第一个作为name,如果没有标签则使用用户名
|
||||||
|
const firstTag = tags ? tags.split(',')[0].trim() : '';
|
||||||
|
const autoName = firstTag || username || '未命名';
|
||||||
|
|
||||||
|
const account = {
|
||||||
|
vault_id: selectedVaultId,
|
||||||
|
name: autoName,
|
||||||
|
username: username,
|
||||||
|
password: document.getElementById('password').value,
|
||||||
|
totp_secret: document.getElementById('totpSecret').value.trim(),
|
||||||
|
tags: tags,
|
||||||
|
email: document.getElementById('email').value.trim(),
|
||||||
|
proxy: document.getElementById('proxy').value.trim(),
|
||||||
|
notes: document.getElementById('notes').value.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingId) {
|
||||||
|
const existingAcc = accounts.find(a => a.id === editingId);
|
||||||
|
if (existingAcc) {
|
||||||
|
account.vault_id = existingAcc.vault_id;
|
||||||
|
}
|
||||||
|
await window.api.updateAccount(editingId, account);
|
||||||
|
showToast('账号已更新', 'success');
|
||||||
|
} else {
|
||||||
|
await window.api.addAccount(account);
|
||||||
|
showToast('账号已添加', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
loadAccounts();
|
||||||
|
loadVaults();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Save failed:', e);
|
||||||
|
showToast('保存失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editAccount(id) {
|
||||||
|
const acc = accounts.find(a => a.id === id);
|
||||||
|
if (!acc) return;
|
||||||
|
|
||||||
|
editingId = id;
|
||||||
|
formTitle.innerHTML = '<i class="fa-solid fa-pen-to-square"></i> 编辑账号';
|
||||||
|
|
||||||
|
document.getElementById('accountId').value = id;
|
||||||
|
document.getElementById('username').value = acc.username || '';
|
||||||
|
document.getElementById('password').value = acc.password || '';
|
||||||
|
document.getElementById('totpSecret').value = acc.totp_secret || '';
|
||||||
|
document.getElementById('email').value = acc.email || '';
|
||||||
|
document.getElementById('proxy').value = acc.proxy || '';
|
||||||
|
document.getElementById('tags').value = acc.tags || '';
|
||||||
|
document.getElementById('notes').value = acc.notes || '';
|
||||||
|
|
||||||
|
document.querySelectorAll('.account-card').forEach(card => {
|
||||||
|
card.classList.toggle('active', card.dataset.id == id);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('username').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAccount(id) {
|
||||||
|
if (!confirm('确定要删除这个账号吗?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.api.deleteAccount(id);
|
||||||
|
showToast('账号已删除', 'success');
|
||||||
|
|
||||||
|
if (editingId === id) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAccounts();
|
||||||
|
loadVaults();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Delete failed:', e);
|
||||||
|
showToast('删除失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
editingId = null;
|
||||||
|
formTitle.innerHTML = '<i class="fa-solid fa-user-plus"></i> 添加账号';
|
||||||
|
accountForm.reset();
|
||||||
|
document.getElementById('accountId').value = '';
|
||||||
|
|
||||||
|
document.querySelectorAll('.account-card').forEach(card => {
|
||||||
|
card.classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await window.api.copyToClipboard(text);
|
||||||
|
showToast('已复制', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Copy failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机名称 (不含符号的长用户名)
|
||||||
|
function generateRandomName() {
|
||||||
|
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
|
||||||
|
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
|
||||||
|
const suffix = Math.floor(Math.random() * 10000);
|
||||||
|
const year = 1990 + Math.floor(Math.random() * 30);
|
||||||
|
|
||||||
|
document.getElementById('randomFullName').value = `${firstName} ${lastName}`;
|
||||||
|
|
||||||
|
// 不含下划线和符号的用户名格式
|
||||||
|
const usernameFormats = [
|
||||||
|
`${firstName.toLowerCase()}${lastName.toLowerCase()}${suffix}`,
|
||||||
|
`${firstName.toLowerCase()}${lastName.toLowerCase()}${year}`,
|
||||||
|
`${firstName.toLowerCase()}${lastName.toLowerCase()}`,
|
||||||
|
`${firstName[0].toLowerCase()}${lastName.toLowerCase()}${suffix}`,
|
||||||
|
`${lastName.toLowerCase()}${firstName.toLowerCase()}${suffix}`,
|
||||||
|
`${firstName.toLowerCase()}${lastName.toLowerCase()}official`,
|
||||||
|
`real${firstName.toLowerCase()}${lastName.toLowerCase()}`
|
||||||
|
];
|
||||||
|
const username = usernameFormats[Math.floor(Math.random() * usernameFormats.length)];
|
||||||
|
document.getElementById('randomUsername').value = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomPassword() {
|
||||||
|
const length = parseInt(document.getElementById('pwdLength').value) || 16;
|
||||||
|
const useUppercase = document.getElementById('pwdUppercase').checked;
|
||||||
|
const useLowercase = document.getElementById('pwdLowercase').checked;
|
||||||
|
const useNumbers = document.getElementById('pwdNumbers').checked;
|
||||||
|
const useSymbols = document.getElementById('pwdSymbols').checked;
|
||||||
|
|
||||||
|
let chars = '';
|
||||||
|
if (useUppercase) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
if (useLowercase) chars += 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
if (useNumbers) chars += '0123456789';
|
||||||
|
if (useSymbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
|
||||||
|
if (!chars) chars = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
|
||||||
|
let password = '';
|
||||||
|
const array = new Uint32Array(length);
|
||||||
|
crypto.getRandomValues(array);
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
password += chars[array[i] % chars.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('randomPassword').value = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.className = 'toast show';
|
||||||
|
if (type === 'success') toast.classList.add('success');
|
||||||
|
setTimeout(() => { toast.classList.remove('show'); }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeJs(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
1305
src/styles.css
Normal file
1305
src/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
3
start.vbs
Normal file
3
start.vbs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Set WshShell = CreateObject("WScript.Shell")
|
||||||
|
WshShell.CurrentDirectory = CreateObject("Scripting.FileSystemObject").GetParentFolderName(WScript.ScriptFullName)
|
||||||
|
WshShell.Run "cmd /c npm start", 0, False
|
||||||
24
stop.bat
Normal file
24
stop.bat
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
@echo off
|
||||||
|
echo 正在关闭 2FA Account Tool...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 终止所有 electron 相关进程
|
||||||
|
taskkill /f /im electron.exe >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo [已关闭] electron.exe
|
||||||
|
) else (
|
||||||
|
echo [未运行] electron.exe
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 可能有残留的 node 进程
|
||||||
|
taskkill /f /im node.exe >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo [已关闭] node.exe
|
||||||
|
) else (
|
||||||
|
echo [未运行] node.exe
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 程序已彻底关闭,现在可以安全删除 node_modules 文件夹了。
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
6
一键导出账号.bat
Normal file
6
一键导出账号.bat
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@echo off
|
||||||
|
echo 正在准备导出账号数据...
|
||||||
|
node export.js
|
||||||
|
echo.
|
||||||
|
echo 导出完成!按任意键退出。
|
||||||
|
pause > nul
|
||||||
Reference in New Issue
Block a user