MrTerm: SSH/SFTP/RDP-Client im Termius-Stil (Electron)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// Persistenter Vault: Hosts, Keys, Snippets, Forwards, Settings.
|
||||
// Wird mit Electron safeStorage (DPAPI unter Windows, libsecret/kwallet unter Linux) verschlüsselt.
|
||||
const { app, safeStorage } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DEFAULTS = {
|
||||
version: 1,
|
||||
groups: [],
|
||||
hosts: [],
|
||||
keys: [],
|
||||
snippets: [],
|
||||
forwards: [],
|
||||
knownHosts: {},
|
||||
history: [],
|
||||
settings: {
|
||||
terminalTheme: 'mrterm',
|
||||
fontFamily: 'Cascadia Code, JetBrains Mono, Fira Code, Consolas, monospace',
|
||||
fontSize: 14,
|
||||
cursorStyle: 'block',
|
||||
cursorBlink: true,
|
||||
scrollback: 10000,
|
||||
copyOnSelect: true,
|
||||
keepAlive: 30,
|
||||
rdpClientLinux: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.dir = app.getPath('userData');
|
||||
this.file = path.join(this.dir, 'vault.dat');
|
||||
this.data = structuredClone(DEFAULTS);
|
||||
this.encrypted = false;
|
||||
}
|
||||
|
||||
canEncrypt() {
|
||||
try {
|
||||
if (!safeStorage.isEncryptionAvailable()) return false;
|
||||
// Unter Linux ohne Keyring fällt Electron auf "basic_text" zurück – das ist keine echte Verschlüsselung.
|
||||
if (process.platform === 'linux' && safeStorage.getSelectedStorageBackend?.() === 'basic_text') return false;
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
load() {
|
||||
fs.mkdirSync(this.dir, { recursive: true });
|
||||
if (!fs.existsSync(this.file)) { this.save(); return this.data; }
|
||||
const raw = fs.readFileSync(this.file);
|
||||
let json;
|
||||
if (raw.slice(0, 4).toString() === 'ENC1') {
|
||||
json = safeStorage.decryptString(raw.slice(4));
|
||||
this.encrypted = true;
|
||||
} else {
|
||||
json = raw.toString('utf8');
|
||||
}
|
||||
const parsed = JSON.parse(json);
|
||||
this.data = { ...structuredClone(DEFAULTS), ...parsed, settings: { ...DEFAULTS.settings, ...(parsed.settings || {}) } };
|
||||
return this.data;
|
||||
}
|
||||
|
||||
save() {
|
||||
const json = JSON.stringify(this.data, null, 2);
|
||||
const tmp = this.file + '.tmp';
|
||||
if (this.canEncrypt()) {
|
||||
fs.writeFileSync(tmp, Buffer.concat([Buffer.from('ENC1'), safeStorage.encryptString(json)]));
|
||||
this.encrypted = true;
|
||||
} else {
|
||||
fs.writeFileSync(tmp, json, { mode: 0o600 });
|
||||
this.encrypted = false;
|
||||
}
|
||||
fs.renameSync(tmp, this.file);
|
||||
}
|
||||
|
||||
get() { return this.data; }
|
||||
|
||||
// Generisches Upsert für Collections (hosts, groups, keys, snippets, forwards)
|
||||
upsert(collection, item) {
|
||||
const list = this.data[collection];
|
||||
if (!Array.isArray(list)) throw new Error('Unbekannte Collection: ' + collection);
|
||||
if (!item.id) item.id = crypto.randomUUID();
|
||||
const i = list.findIndex((x) => x.id === item.id);
|
||||
if (i >= 0) list[i] = { ...list[i], ...item, updatedAt: Date.now() };
|
||||
else list.push({ ...item, createdAt: Date.now(), updatedAt: Date.now() });
|
||||
this.save();
|
||||
return item;
|
||||
}
|
||||
|
||||
remove(collection, id) {
|
||||
this.data[collection] = this.data[collection].filter((x) => x.id !== id);
|
||||
if (collection === 'groups') this.data.hosts.forEach((h) => { if (h.groupId === id) h.groupId = null; });
|
||||
this.save();
|
||||
}
|
||||
|
||||
setSettings(s) { this.data.settings = { ...this.data.settings, ...s }; this.save(); }
|
||||
|
||||
addHistory(entry) {
|
||||
this.data.history = [entry, ...this.data.history.filter((h) => h.hostId !== entry.hostId)].slice(0, 30);
|
||||
this.save();
|
||||
}
|
||||
|
||||
resolveHost(id) { return this.data.hosts.find((h) => h.id === id); }
|
||||
resolveKey(id) { return this.data.keys.find((k) => k.id === id); }
|
||||
}
|
||||
|
||||
module.exports = { Store };
|
||||
Reference in New Issue
Block a user