Files
deskhub/desktop/src/main.js
T
2026-10-19 18:06:20 +00:00

187 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { app, BrowserWindow, ipcMain, safeStorage, Notification, shell, Menu, Tray, nativeImage, nativeTheme } = require('electron');
const path = require('path');
const fs = require('fs');
app.setAppUserModelId('de.deskhub.desktop');
let win = null;
const sessionFile = () => path.join(app.getPath('userData'), 'session.bin');
// ─── Sitzung (Token DPAPI-verschlüsselt über safeStorage) ───────────────────
let session = null; // { serverUrl, token, expiresAt, staff }
function loadSession() {
try {
const raw = fs.readFileSync(sessionFile());
session = JSON.parse(safeStorage.isEncryptionAvailable() ? safeStorage.decryptString(raw) : raw.toString('utf8'));
} catch { session = null; }
}
function saveSession() {
if (!session) { try { fs.unlinkSync(sessionFile()); } catch {} return; }
const json = JSON.stringify(session);
fs.writeFileSync(sessionFile(), safeStorage.isEncryptionAvailable() ? safeStorage.encryptString(json) : json);
}
function normalizeUrl(input) {
let s = String(input || '').trim();
if (!s) throw new Error('Bitte Server-Adresse eingeben.');
if (!s.includes('://')) s = 'https://' + s;
const u = new URL(s);
if (u.protocol !== 'https:') throw new Error('Die Server-Adresse muss HTTPS verwenden.');
if (u.username || u.search || u.hash) throw new Error('Die Server-Adresse ist ungültig.');
return u.origin + u.pathname.replace(/\/+$/, '');
}
async function callApi({ path: p, method = 'GET', query, body, auth = true, serverUrl }) {
const base = serverUrl || session?.serverUrl;
if (!base) throw { status: 401, code: 'UNAUTHENTICATED', message: 'Nicht angemeldet.' };
const url = new URL(`${base}/api/staff/v1/${p}`);
for (const [k, v] of Object.entries(query || {})) if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v));
const headers = { Accept: 'application/json', 'Cache-Control': 'no-cache' };
if (auth) {
if (!session?.token) throw { status: 401, code: 'UNAUTHENTICATED', message: 'Nicht angemeldet.' };
headers.Authorization = `Bearer ${session.token}`;
headers['X-DeskHub-Authorization'] = `Bearer ${session.token}`; // Ersatz-Header: viele Hoster entfernen "Authorization"
}
const init = { method, headers };
if (body !== undefined && method !== 'GET') { headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); }
let res;
try { res = await fetch(url, { ...init, signal: AbortSignal.timeout(30000) }); }
catch (e) { throw { status: 0, code: 'NETWORK', message: 'Server nicht erreichbar.' }; }
const text = await res.text();
let data = null;
try { data = text ? JSON.parse(text) : null; } catch {}
if (!res.ok && res.status !== 202) {
if (res.status === 401 && auth) { session = null; saveSession(); win?.webContents.send('session-expired'); }
throw { status: res.status, code: data?.error?.code || `HTTP_${res.status}`, message: data?.error?.message || `Serverfehler (${res.status}).` };
}
return { status: res.status, data };
}
ipcMain.handle('session:get', () => session ? { serverUrl: session.serverUrl, staff: session.staff } : null);
ipcMain.handle('auth:login', async (_e, { serverUrl, email, password, totpCode }) => {
try {
const base = normalizeUrl(serverUrl);
const r = await callApi({
path: 'login.php', method: 'POST', auth: false, serverUrl: base,
body: { email, password, totpCode: totpCode || null, deviceName: `Windows Desktop (${require('os').hostname()})` },
});
if (r.status === 202 || r.data?.requiresTotp) return { requiresTotp: true };
session = { serverUrl: base, token: r.data.token, expiresAt: r.data.expiresAt, staff: r.data.staff };
saveSession();
return { staff: session.staff };
} catch (e) { return { error: e.message || String(e) }; }
});
ipcMain.handle('auth:logout', async () => {
try { await callApi({ path: 'logout.php', method: 'POST', body: {} }); } catch {}
session = null; saveSession();
return true;
});
ipcMain.handle('api', async (_e, req) => {
if (typeof req?.path !== 'string' || !/^[a-z_]+\.php$/.test(req.path)) return { error: { message: 'Ungültiger Pfad.' } };
try {
const r = await callApi(req);
if (req.path === 'me.php' && r.data?.staff && session) { session.staff = r.data.staff; saveSession(); }
return { data: r.data };
} catch (e) { return { error: { status: e.status, code: e.code, message: e.message || String(e) } }; }
});
ipcMain.handle('notify', (_e, { title, body }) => {
if (Notification.isSupported()) {
const n = new Notification({ title: String(title).slice(0, 100), body: String(body).slice(0, 200) });
n.on('click', () => showWindow('tickets'));
n.show();
}
});
// ─── Einstellungen (settings.json im Benutzerordner) ────────────────────────
const DEFAULTS = { theme: 'system', autostart: false, closeToTray: true, notifications: true, pollSeconds: 60, startMinimized: false, bounds: null };
let settings = { ...DEFAULTS };
const settingsFile = () => path.join(app.getPath('userData'), 'settings.json');
function loadSettings() { try { settings = { ...DEFAULTS, ...JSON.parse(fs.readFileSync(settingsFile(), 'utf8')) }; } catch { settings = { ...DEFAULTS }; } }
function saveSettings() { try { fs.writeFileSync(settingsFile(), JSON.stringify(settings)); } catch {} }
function applyAutostart() {
if (!app.isPackaged) return;
app.setLoginItemSettings({ openAtLogin: !!settings.autostart, args: settings.autostart ? ['--hidden'] : [] });
}
ipcMain.handle('settings:get', () => ({ ...settings, version: app.getVersion(), packaged: app.isPackaged }));
ipcMain.handle('settings:set', (_e, patch) => {
for (const k of Object.keys(DEFAULTS)) if (k in patch && k !== 'bounds') settings[k] = patch[k];
saveSettings(); applyAutostart();
applyNativeTheme();
return { ...settings, version: app.getVersion(), packaged: app.isPackaged };
});
// ─── Tray & Zähler ──────────────────────────────────────────────────────────
let tray = null, quitting = false;
function showWindow(page) {
if (!win) return;
win.show(); if (win.isMinimized()) win.restore(); win.focus();
if (page) win.webContents.send('navigate', page);
}
function createTray() {
const img = nativeImage.createFromPath(path.join(__dirname, 'assets', 'icon.ico'));
tray = new Tray(img.resize({ width: 16, height: 16 }));
tray.setToolTip('DeskHub');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'DeskHub öffnen', click: () => showWindow() },
{ label: 'Tickets', click: () => showWindow('tickets') },
{ label: 'Neues Ticket', click: () => showWindow('newticket') },
{ type: 'separator' },
{ label: 'Beenden', click: () => { quitting = true; app.quit(); } },
]));
tray.on('click', () => showWindow());
}
ipcMain.handle('badge', (_e, { count, dataUrl }) => {
if (!win) return;
try {
win.setOverlayIcon(count > 0 && dataUrl ? nativeImage.createFromDataURL(dataUrl) : null, count > 0 ? `${count} neu` : '');
tray?.setToolTip(count > 0 ? `DeskHub – ${count} neu` : 'DeskHub');
} catch {}
});
// Hell/Dunkel: "system" folgt Windows; die Titelleiste (Fensterknöpfe) wird passend zum Design eingefärbt.
const resolvedTheme = () => (settings.theme === 'system' ? (nativeTheme.shouldUseDarkColors ? 'dark' : 'light') : settings.theme);
const overlayFor = (theme) => (theme === 'light'
? { color: '#28405a', symbolColor: '#e9eff6', height: 54 }
: { color: '#0a1119', symbolColor: '#e4ebf3', height: 54 });
function applyNativeTheme() {
nativeTheme.themeSource = ['light', 'dark'].includes(settings.theme) ? settings.theme : 'system';
try { win?.setTitleBarOverlay(overlayFor(resolvedTheme())); win?.setBackgroundColor(resolvedTheme() === 'light' ? '#eef1f5' : '#0d141d'); } catch {}
}
nativeTheme.on('updated', () => { try { win?.setTitleBarOverlay(overlayFor(resolvedTheme())); } catch {} });
function createWindow() {
const b = settings.bounds || {};
win = new BrowserWindow({
width: b.width || 1320, height: b.height || 840, x: b.x, y: b.y, minWidth: 960, minHeight: 620,
show: false, titleBarStyle: 'hidden', titleBarOverlay: overlayFor(resolvedTheme()), backgroundColor: resolvedTheme() === 'light' ? '#eef1f5' : '#0d141d', title: 'DeskHub', autoHideMenuBar: true,
icon: path.join(__dirname, 'assets', 'icon.ico'),
webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false },
});
if (b.maximized) win.maximize();
win.loadFile(path.join(__dirname, 'index.html'));
win.once('ready-to-show', () => { if (!(process.argv.includes('--hidden') && settings.autostart)) win.show(); });
const saveBounds = () => { if (!win || win.isMinimized()) return; settings.bounds = { ...win.getNormalBounds(), maximized: win.isMaximized() }; saveSettings(); };
win.on('resized', saveBounds); win.on('moved', saveBounds); win.on('maximize', saveBounds); win.on('unmaximize', saveBounds);
win.on('close', (e) => { if (!quitting && settings.closeToTray && tray) { e.preventDefault(); win.hide(); } });
win.on('closed', () => { win = null; });
// Externe Links nie im App-Fenster öffnen
win.webContents.setWindowOpenHandler(({ url }) => { if (/^https:/i.test(url)) shell.openExternal(url); return { action: 'deny' }; });
win.webContents.on('will-navigate', (e, url) => { if (!url.startsWith('file://')) e.preventDefault(); });
}
// Vom Setup einmalig aufgerufen: Windows prüft die neue EXE dabei (Defender), der echte Start ist danach schnell.
if (process.argv.includes('--prewarm')) app.exit(0);
const lock = app.requestSingleInstanceLock();
if (!lock) app.quit();
else {
app.on('second-instance', () => showWindow());
app.on('before-quit', () => { quitting = true; });
app.whenReady().then(() => { Menu.setApplicationMenu(null); loadSettings(); loadSession(); createWindow(); setImmediate(() => { createTray(); applyAutostart(); }); });
app.on('window-all-closed', () => { if (quitting || !settings.closeToTray) app.quit(); });
}