feat: 增加主密码功能
This commit is contained in:
@@ -151,4 +151,26 @@ export function registerIpcHandlers() {
|
||||
return true;
|
||||
});
|
||||
|
||||
// ==================== 安全管理 ====================
|
||||
|
||||
ipcMain.handle('mqtt:get-security-status', () => {
|
||||
return mqttManager.getSecurityStatus();
|
||||
});
|
||||
|
||||
ipcMain.handle('mqtt:setup-master-password', (_, password) => {
|
||||
return mqttManager.setupMasterPassword(password);
|
||||
});
|
||||
|
||||
ipcMain.handle('mqtt:unlock', (_, password) => {
|
||||
return mqttManager.unlock(password);
|
||||
});
|
||||
|
||||
ipcMain.handle('mqtt:lock', () => {
|
||||
return mqttManager.lock();
|
||||
});
|
||||
|
||||
ipcMain.handle('mqtt:remove-master-password', (_, password) => {
|
||||
return mqttManager.removeMasterPassword(password);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import { app } from 'electron';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { createPasswordVerifier, decryptSecret, deriveKey, encryptSecret, verifyPassword } from './security.js';
|
||||
|
||||
import mqtt from 'mqtt';
|
||||
|
||||
@@ -36,6 +38,9 @@ class MqttManager {
|
||||
this.msgIdCounter = 0; // 用于生成唯一消息 id
|
||||
this.dataPath = join(app.getPath('userData'), 'frost-mqtt-data.json');
|
||||
this.quickSendList = [];
|
||||
this.security = null;
|
||||
this.masterKey = null;
|
||||
this.locked = false;
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
@@ -77,16 +82,30 @@ class MqttManager {
|
||||
let raw = readFileSync(this.dataPath, 'utf-8');
|
||||
let data = JSON.parse(raw);
|
||||
|
||||
this.security = data.security || null;
|
||||
this.locked = Boolean(this.security?.enabled);
|
||||
|
||||
if (data.servers) {
|
||||
data.servers.forEach((s) => {
|
||||
|
||||
let encryptedPassword = s.passwordEncrypted || '';
|
||||
|
||||
s.status = 'disconnected';
|
||||
s.password = this.locked ? '' : (s.password || '');
|
||||
|
||||
if (encryptedPassword) {
|
||||
s.passwordEncrypted = encryptedPassword;
|
||||
}
|
||||
|
||||
s.topics = (s.topics || []).map((t) => ({
|
||||
...t,
|
||||
id: t.id || 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
|
||||
comment: t.comment ?? '',
|
||||
subscribed: false,
|
||||
}));
|
||||
|
||||
this.servers.set(s.id, s);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,8 +143,13 @@ class MqttManager {
|
||||
try {
|
||||
|
||||
let data = {
|
||||
security: this.security,
|
||||
servers: Array.from(this.servers.values()).map((s) => ({
|
||||
...s,
|
||||
password: this.security?.enabled ? undefined : (s.password || ''),
|
||||
passwordEncrypted: this.security?.enabled && s.password
|
||||
? encryptSecret(s.password, this.masterKey)
|
||||
: (this.security?.enabled ? s.passwordEncrypted || '' : undefined),
|
||||
status: 'disconnected',
|
||||
topics: s.topics.map((t) => ({ ...t, subscribed: false })),
|
||||
})),
|
||||
@@ -214,11 +238,109 @@ class MqttManager {
|
||||
|
||||
// ==================== 服务器管理 ====================
|
||||
|
||||
getSecurityStatus() {
|
||||
return { enabled: Boolean(this.security?.enabled), locked: this.locked };
|
||||
}
|
||||
|
||||
setupMasterPassword(password) {
|
||||
|
||||
this.assertPassword(password);
|
||||
|
||||
if (this.security?.enabled) {
|
||||
throw new Error('主密码已设置');
|
||||
}
|
||||
|
||||
let salt = randomBytes(16).toString('base64');
|
||||
|
||||
this.masterKey = deriveKey(password, Buffer.from(salt, 'base64'));
|
||||
this.security = { enabled: true, salt, verifier: createPasswordVerifier(this.masterKey) };
|
||||
this.locked = false;
|
||||
this.saveData();
|
||||
|
||||
return this.getSecurityStatus();
|
||||
|
||||
}
|
||||
|
||||
unlock(password) {
|
||||
|
||||
if (!this.security?.enabled) {
|
||||
return this.getSecurityStatus();
|
||||
};
|
||||
|
||||
let key = deriveKey(password, Buffer.from(this.security.salt, 'base64'));
|
||||
|
||||
if (!verifyPassword(key, this.security.verifier)) {
|
||||
throw new Error('主密码错误');
|
||||
}
|
||||
|
||||
try {
|
||||
this.servers.forEach((server) => {
|
||||
server.password = server.passwordEncrypted ? decryptSecret(server.passwordEncrypted, key) : '';
|
||||
});
|
||||
} catch {
|
||||
throw new Error('服务器密码解密失败,数据可能已损坏');
|
||||
}
|
||||
|
||||
this.masterKey = key;
|
||||
this.locked = false;
|
||||
|
||||
return this.getSecurityStatus();
|
||||
|
||||
}
|
||||
|
||||
lock() {
|
||||
|
||||
if (!this.security?.enabled || this.locked) {
|
||||
return this.getSecurityStatus();
|
||||
}
|
||||
|
||||
this.clients.forEach((_, id) => this.disconnect(id));
|
||||
this.servers.forEach((server) => {
|
||||
server.passwordEncrypted = server.password
|
||||
? encryptSecret(server.password, this.masterKey)
|
||||
: server.passwordEncrypted || '';
|
||||
server.password = '';
|
||||
});
|
||||
this.saveData();
|
||||
this.masterKey = null;
|
||||
this.locked = true;
|
||||
|
||||
return this.getSecurityStatus();
|
||||
|
||||
}
|
||||
|
||||
removeMasterPassword(password) {
|
||||
|
||||
if (!this.security?.enabled) {
|
||||
return this.getSecurityStatus();
|
||||
}
|
||||
|
||||
this.unlock(password);
|
||||
this.security = null;
|
||||
this.masterKey = null;
|
||||
this.locked = false;
|
||||
this.saveData();
|
||||
|
||||
return this.getSecurityStatus();
|
||||
|
||||
}
|
||||
|
||||
assertPassword(password) {
|
||||
if (typeof password !== 'string' || password.length < 4) {
|
||||
throw new Error('主密码至少需要 4 个字符');
|
||||
}
|
||||
}
|
||||
|
||||
assertUnlocked() {
|
||||
if (this.locked) throw new Error('应用已锁定,请先解锁');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有服务器列表
|
||||
* @returns {MqttServer[]} 服务器配置数组
|
||||
*/
|
||||
getServers() {
|
||||
this.assertUnlocked();
|
||||
return Array.from(this.servers.values()).map((s) => ({
|
||||
...s,
|
||||
password: s.password || '',
|
||||
@@ -231,6 +353,7 @@ class MqttManager {
|
||||
* @returns {MqttServer | null} 服务器配置或 null
|
||||
*/
|
||||
getServer(id) {
|
||||
this.assertUnlocked();
|
||||
return this.servers.get(id) || null;
|
||||
}
|
||||
|
||||
@@ -293,6 +416,8 @@ class MqttManager {
|
||||
*/
|
||||
addServer(config) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
||||
let server = {
|
||||
id,
|
||||
@@ -318,6 +443,8 @@ class MqttManager {
|
||||
*/
|
||||
updateServer(id, config) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let server = this.servers.get(id);
|
||||
|
||||
if (!server) {
|
||||
@@ -337,6 +464,7 @@ class MqttManager {
|
||||
* @param {string} id - 服务器 id
|
||||
*/
|
||||
deleteServer(id) {
|
||||
this.assertUnlocked();
|
||||
// 先断开连接
|
||||
this.disconnect(id);
|
||||
this.servers.delete(id);
|
||||
@@ -350,7 +478,11 @@ class MqttManager {
|
||||
* @returns {ServerConfig[]} 服务器配置数组
|
||||
*/
|
||||
exportServers() {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let schemaKeys = Object.keys(this.getServerConfigSchema());
|
||||
|
||||
return Array.from(this.servers.values()).map((s) => {
|
||||
let exported = {};
|
||||
for (let key of schemaKeys) {
|
||||
@@ -359,6 +491,7 @@ class MqttManager {
|
||||
exported.topics = (s.topics || []).map((t) => this.buildTopicExport(t));
|
||||
return exported;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,6 +514,9 @@ class MqttManager {
|
||||
* @returns {MqttServer[]} 导入后的服务器列表
|
||||
*/
|
||||
importServers(serverList) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
if (!Array.isArray(serverList)) {
|
||||
throw new Error('导入格式错误:必须是服务器配置数组');
|
||||
}
|
||||
@@ -396,6 +532,7 @@ class MqttManager {
|
||||
this.publishHistory.clear();
|
||||
|
||||
serverList.forEach((config) => {
|
||||
|
||||
if (!config.name || !config.host) {
|
||||
return;
|
||||
}
|
||||
@@ -411,10 +548,13 @@ class MqttManager {
|
||||
this.servers.set(id, server);
|
||||
this.messages.set(id, []);
|
||||
this.publishHistory.set(id, []);
|
||||
|
||||
});
|
||||
|
||||
this.saveData();
|
||||
|
||||
return this.getServers();
|
||||
|
||||
}
|
||||
|
||||
// ==================== 快速发送 ====================
|
||||
@@ -424,6 +564,7 @@ class MqttManager {
|
||||
* @returns {QuickSendItem[]} 快速发送项数组
|
||||
*/
|
||||
getQuickSendList() {
|
||||
this.assertUnlocked();
|
||||
return this.quickSendList;
|
||||
}
|
||||
|
||||
@@ -432,13 +573,19 @@ class MqttManager {
|
||||
* @param {QuickSendItem[]} list - 快速发送项数组
|
||||
*/
|
||||
saveQuickSendList(list) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.quickSendList = list.filter((item) => {
|
||||
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
||||
});
|
||||
|
||||
this.saveData();
|
||||
|
||||
}
|
||||
|
||||
// ==================== 连接管理 ====================
|
||||
@@ -450,6 +597,8 @@ class MqttManager {
|
||||
*/
|
||||
connect(id) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let server = this.servers.get(id);
|
||||
|
||||
if (!server) {
|
||||
@@ -551,6 +700,8 @@ class MqttManager {
|
||||
*/
|
||||
disconnect(id) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let client = this.clients.get(id);
|
||||
|
||||
if (client) {
|
||||
@@ -614,10 +765,15 @@ class MqttManager {
|
||||
* @returns {MqttTopic | null} 添加后的主题对象或 null
|
||||
*/
|
||||
addTopic(serverId, topic, qos = 1, comment = '') {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let server = this.servers.get(serverId);
|
||||
|
||||
if (!server) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (server.topics.find((t) => t.topic === topic)) {
|
||||
return null;
|
||||
}
|
||||
@@ -646,6 +802,8 @@ class MqttManager {
|
||||
*/
|
||||
removeTopic(serverId, topic) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let server = this.servers.get(serverId);
|
||||
|
||||
if (!server) {
|
||||
@@ -671,6 +829,8 @@ class MqttManager {
|
||||
*/
|
||||
subscribe(serverId, topic) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let server = this.servers.get(serverId);
|
||||
|
||||
if (!server) {
|
||||
@@ -695,6 +855,8 @@ class MqttManager {
|
||||
*/
|
||||
unsubscribe(serverId, topic) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let client = this.clients.get(serverId);
|
||||
let server = this.servers.get(serverId);
|
||||
|
||||
@@ -727,6 +889,8 @@ class MqttManager {
|
||||
*/
|
||||
updateTopic(serverId, topicId, newTopic, newQos, comment = '') {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let server = this.servers.get(serverId);
|
||||
|
||||
if (!server) {
|
||||
@@ -785,6 +949,8 @@ class MqttManager {
|
||||
*/
|
||||
subscribeAll(serverId) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let client = this.clients.get(serverId);
|
||||
let server = this.servers.get(serverId);
|
||||
|
||||
@@ -835,6 +1001,8 @@ class MqttManager {
|
||||
*/
|
||||
publish(serverId, topic, payload, opts = {}) {
|
||||
|
||||
this.assertUnlocked();
|
||||
|
||||
let client = this.clients.get(serverId);
|
||||
let server = this.servers.get(serverId);
|
||||
|
||||
@@ -914,6 +1082,7 @@ class MqttManager {
|
||||
* @returns {MqttMessage[]} 消息数组
|
||||
*/
|
||||
getMessages(serverId) {
|
||||
this.assertUnlocked();
|
||||
return this.messages.get(serverId) || [];
|
||||
}
|
||||
|
||||
@@ -923,6 +1092,7 @@ class MqttManager {
|
||||
* @returns {PublishRecord[]} 发布历史数组
|
||||
*/
|
||||
getPublishHistory(serverId) {
|
||||
this.assertUnlocked();
|
||||
return this.publishHistory.get(serverId) || [];
|
||||
}
|
||||
|
||||
@@ -931,6 +1101,7 @@ class MqttManager {
|
||||
* @param {string} serverId - 服务器 id
|
||||
*/
|
||||
clearMessages(serverId) {
|
||||
this.assertUnlocked();
|
||||
this.messages.set(serverId, []);
|
||||
this.saveData();
|
||||
}
|
||||
@@ -940,6 +1111,7 @@ class MqttManager {
|
||||
* @param {string} serverId - 服务器 id
|
||||
*/
|
||||
clearPublishHistory(serverId) {
|
||||
this.assertUnlocked();
|
||||
this.publishHistory.set(serverId, []);
|
||||
this.saveData();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync, timingSafeEqual } from 'crypto';
|
||||
|
||||
const KEY_LENGTH = 32;
|
||||
const VERIFIER = 'frost-mqtt-master-password-v1';
|
||||
|
||||
export function deriveKey(password, salt) {
|
||||
return scryptSync(password, salt, KEY_LENGTH, { N: 16384, r: 8, p: 1 });
|
||||
}
|
||||
|
||||
export function createPasswordVerifier(key) {
|
||||
return createHash('sha256').update(key).update(VERIFIER).digest('hex');
|
||||
}
|
||||
|
||||
export function verifyPassword(key, verifier) {
|
||||
let expected = Buffer.from(createPasswordVerifier(key), 'hex');
|
||||
let actual = Buffer.from(verifier || '', 'hex');
|
||||
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
||||
}
|
||||
|
||||
export function encryptSecret(secret, key) {
|
||||
let iv = randomBytes(12);
|
||||
let cipher = createCipheriv('aes-256-gcm', key, iv);
|
||||
let encrypted = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
|
||||
return `v1:${iv.toString('base64')}:${cipher.getAuthTag().toString('base64')}:${encrypted.toString('base64')}`;
|
||||
}
|
||||
|
||||
export function decryptSecret(value, key) {
|
||||
let parts = String(value || '').split(':');
|
||||
if (parts.length !== 4 || parts[0] !== 'v1') throw new Error('加密数据格式无效');
|
||||
let decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(parts[1], 'base64'));
|
||||
decipher.setAuthTag(Buffer.from(parts[2], 'base64'));
|
||||
return Buffer.concat([
|
||||
decipher.update(Buffer.from(parts[3], 'base64')),
|
||||
decipher.final(),
|
||||
]).toString('utf8');
|
||||
}
|
||||
@@ -8,6 +8,11 @@ import { electronAPI } from '@electron-toolkit/preload';
|
||||
const api = {
|
||||
|
||||
// 服务器管理
|
||||
getSecurityStatus: () => ipcRenderer.invoke('mqtt:get-security-status'),
|
||||
setupMasterPassword: (password) => ipcRenderer.invoke('mqtt:setup-master-password', password),
|
||||
unlock: (password) => ipcRenderer.invoke('mqtt:unlock', password),
|
||||
lock: () => ipcRenderer.invoke('mqtt:lock'),
|
||||
removeMasterPassword: (password) => ipcRenderer.invoke('mqtt:remove-master-password', password),
|
||||
getServers: () => ipcRenderer.invoke('mqtt:get-servers'),
|
||||
addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config),
|
||||
updateServer: (id, config) => ipcRenderer.invoke('mqtt:update-server', id, config),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { NLayout, NMessageProvider, NDialogProvider, NConfigProvider, NTabs, NTabPane, zhCN, dateZhCN } from 'naive-ui';
|
||||
import { NLayout, NMessageProvider, NDialogProvider, NConfigProvider, NTabs, NTabPane, NCard, NInput, NButton, NSpace, zhCN, dateZhCN } from 'naive-ui';
|
||||
import { useMqttStore } from './stores/mqtt.js';
|
||||
|
||||
import Sidebar from './components/Sidebar.vue';
|
||||
import ServerModal from './components/ServerModal.vue';
|
||||
import SecurityModal from './components/SecurityModal.vue';
|
||||
import Dashboard from './views/Dashboard.vue';
|
||||
import Subscribe from './views/Subscribe.vue';
|
||||
import Publish from './views/Publish.vue';
|
||||
@@ -40,13 +41,19 @@ const themeOverrides = {
|
||||
},
|
||||
};
|
||||
|
||||
// 当前激活的标签页
|
||||
/** 当前激活的标签页 */
|
||||
const activeTab = ref('dashboard');
|
||||
// 是否显示服务器编辑弹窗
|
||||
|
||||
/** 是否显示服务器编辑弹窗 */
|
||||
const showServerModal = ref(false);
|
||||
// 当前编辑的服务器数据,null 表示新增
|
||||
|
||||
/** 当前编辑的服务器数据,null 表示新增 */
|
||||
const editServerData = ref(null);
|
||||
|
||||
const showSecurityModal = ref(false);
|
||||
const unlockPassword = ref('');
|
||||
const unlockError = ref('');
|
||||
|
||||
// 组件挂载时初始化 Store
|
||||
onMounted(async () => {
|
||||
await store.init();
|
||||
@@ -76,6 +83,16 @@ function handleServerSaved() {
|
||||
// 刷新数据
|
||||
}
|
||||
|
||||
async function handleUnlock() {
|
||||
try {
|
||||
await store.unlock(unlockPassword.value);
|
||||
unlockPassword.value = '';
|
||||
unlockError.value = '';
|
||||
} catch (e) {
|
||||
unlockError.value = e.message || '解锁失败';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换标签页
|
||||
* @param {string} tab - 标签页名称
|
||||
@@ -103,10 +120,15 @@ function handleSwitchTab(tab) {
|
||||
<Sidebar
|
||||
@add-server="handleAddServer"
|
||||
@edit-server="handleEditServer"
|
||||
@lock="store.lock"
|
||||
@security="showSecurityModal = true"
|
||||
/>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<n-layout content-class="main-layout-content">
|
||||
<n-layout
|
||||
v-if="!store.security.value.locked"
|
||||
content-class="main-layout-content"
|
||||
>
|
||||
<n-tabs
|
||||
v-model:value="activeTab"
|
||||
type="line"
|
||||
@@ -165,8 +187,41 @@ function handleSwitchTab(tab) {
|
||||
:edit-server="editServerData"
|
||||
@saved="handleServerSaved"
|
||||
/>
|
||||
<SecurityModal v-model:show="showSecurityModal" />
|
||||
|
||||
</n-layout>
|
||||
<div
|
||||
v-if="store.security.value.locked"
|
||||
class="lock-screen"
|
||||
>
|
||||
<n-card
|
||||
title="应用已锁定"
|
||||
class="lock-card"
|
||||
>
|
||||
<p>请输入主密码以继续使用 Frost MQTT Client</p>
|
||||
<n-input
|
||||
v-model:value="unlockPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
autofocus
|
||||
@keyup.enter="handleUnlock"
|
||||
/>
|
||||
<div
|
||||
v-if="unlockError"
|
||||
class="lock-error"
|
||||
>{{ unlockError }}</div>
|
||||
<n-space
|
||||
justify="end"
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!unlockPassword"
|
||||
@click="handleUnlock"
|
||||
>解锁</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-dialog-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
@@ -215,6 +270,22 @@ html, body, #app {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lock-screen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(in srgb, var(--n-color) 88%, transparent);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.lock-card {
|
||||
width: 360px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
box-sizing: border-box;
|
||||
padding: 0 0 16px 0;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { NModal, NCard, NForm, NFormItem, NInput, NButton, NSpace, useMessage } from 'naive-ui';
|
||||
import { useMqttStore } from '../stores/mqtt.js';
|
||||
|
||||
defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:show']);
|
||||
const store = useMqttStore();
|
||||
const msg = useMessage();
|
||||
const password = ref('');
|
||||
const confirmPassword = ref('');
|
||||
|
||||
async function save() {
|
||||
|
||||
if (password.value.length < 4 || password.value !== confirmPassword.value) {
|
||||
msg.error(password.value.length < 4 ? '主密码至少需要 4 个字符' : '两次输入的主密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await store.setupMasterPassword(password.value);
|
||||
password.value = '';
|
||||
confirmPassword.value = '';
|
||||
emit('update:show', false);
|
||||
msg.success('主密码已设置,服务器密码已加密保存');
|
||||
} catch (e) {
|
||||
msg.error(e.message || '设置失败');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
try {
|
||||
await store.removeMasterPassword(password.value);
|
||||
password.value = '';
|
||||
confirmPassword.value = '';
|
||||
emit('update:show', false);
|
||||
msg.success('主密码已移除');
|
||||
} catch (e) {
|
||||
msg.error(e.message || '移除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
:show="show"
|
||||
:mask-closable="false"
|
||||
@update:show="$emit('update:show', $event)"
|
||||
>
|
||||
<n-card
|
||||
style="width: 420px; max-width: 90vw"
|
||||
:title="store.security.value.enabled ? '移除主密码' : '设置主密码'"
|
||||
>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="主密码">
|
||||
<n-input
|
||||
v-model:value="password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
autofocus
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
v-if="!store.security.value.enabled"
|
||||
label="确认主密码"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="confirmPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
@keyup.enter="save"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="$emit('update:show', false)">取消</n-button>
|
||||
<n-button
|
||||
v-if="store.security.value.enabled"
|
||||
type="error"
|
||||
@click="remove"
|
||||
>移除并解密保存</n-button>
|
||||
<n-button
|
||||
v-else
|
||||
type="primary"
|
||||
@click="save"
|
||||
>设置</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -4,7 +4,7 @@ import { NLayoutSider, NButton, NTag, useMessage, useDialog } from 'naive-ui';
|
||||
import { useMqttStore } from '../stores/mqtt.js';
|
||||
|
||||
// 组件事件定义
|
||||
const emit = defineEmits(['add-server', 'edit-server']);
|
||||
const emit = defineEmits(['add-server', 'edit-server', 'lock', 'security']);
|
||||
|
||||
// 全局状态与消息/对话框 API
|
||||
const store = useMqttStore();
|
||||
@@ -203,6 +203,22 @@ async function handleImportFile(event) {
|
||||
@click="handleExport"
|
||||
>导出配置</n-button>
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-import-export"
|
||||
:class="{ 'sidebar-single-action': !store.security.value.enabled }"
|
||||
>
|
||||
<n-button
|
||||
v-if="store.security.value.enabled"
|
||||
size="small"
|
||||
block
|
||||
@click="emit('lock')"
|
||||
>锁定应用</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
block
|
||||
@click="emit('security')"
|
||||
>{{ store.security.value.enabled ? '主密码设置' : '设置主密码' }}</n-button>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
@@ -389,4 +405,8 @@ async function handleImportFile(event) {
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sidebar-single-action {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -103,6 +103,8 @@ const publishHistory = ref([]);
|
||||
*/
|
||||
const loading = ref(false);
|
||||
|
||||
const security = ref({ enabled: false, locked: false });
|
||||
|
||||
/**
|
||||
* IPC 监听器清理函数集合
|
||||
* @type {Array<() => void>}
|
||||
@@ -264,6 +266,12 @@ export function useMqttStore() {
|
||||
|
||||
try {
|
||||
|
||||
security.value = await window.api.getSecurityStatus();
|
||||
|
||||
if (security.value.locked) {
|
||||
return;
|
||||
};
|
||||
|
||||
let data = await window.api.getServers();
|
||||
|
||||
servers.value = data;
|
||||
@@ -279,6 +287,32 @@ export function useMqttStore() {
|
||||
|
||||
}
|
||||
|
||||
async function unlock(password) {
|
||||
await window.api.unlock(password);
|
||||
security.value = await window.api.getSecurityStatus();
|
||||
let data = await window.api.getServers();
|
||||
servers.value = data;
|
||||
sortServers();
|
||||
servers.value.forEach((srv) => sortTopics(srv));
|
||||
setupListeners();
|
||||
}
|
||||
|
||||
async function setupMasterPassword(password) {
|
||||
security.value = await window.api.setupMasterPassword(password);
|
||||
}
|
||||
|
||||
async function lock() {
|
||||
security.value = await window.api.lock();
|
||||
servers.value = [];
|
||||
activeServerId.value = null;
|
||||
messages.value = [];
|
||||
publishHistory.value = [];
|
||||
}
|
||||
|
||||
async function removeMasterPassword(password) {
|
||||
security.value = await window.api.removeMasterPassword(password);
|
||||
}
|
||||
|
||||
// ==================== 服务器管理 ====================
|
||||
|
||||
/**
|
||||
@@ -558,6 +592,7 @@ export function useMqttStore() {
|
||||
messages,
|
||||
publishHistory,
|
||||
loading,
|
||||
security,
|
||||
// 计算属性
|
||||
connectedServers,
|
||||
totalTopics,
|
||||
@@ -565,6 +600,10 @@ export function useMqttStore() {
|
||||
totalMessages,
|
||||
// 方法
|
||||
init,
|
||||
unlock,
|
||||
setupMasterPassword,
|
||||
lock,
|
||||
removeMasterPassword,
|
||||
setActiveServer,
|
||||
addServer,
|
||||
updateServer,
|
||||
|
||||
Reference in New Issue
Block a user