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) } }; } }); // Seite der Weboberfläche im Standardbrowser öffnen: Der Server gibt eine einmalige Anmelde-Adresse (und prüft dabei die Rechte) ipcMain.handle('web:open', async (_e, { path: p }) => { if (typeof p !== 'string' || !p.startsWith('/staff/') || p.length > 200) return { error: { message: 'Ungültige Seite.' } }; try { const r = await callApi({ path: 'web_pages.php', method: 'POST', body: { path: p } }); const u = new URL(String(r.data?.url || '')); const base = new URL(session.serverUrl); if (u.protocol !== 'https:' || u.origin !== base.origin || !u.pathname.endsWith('/staff/app_login.php')) return { error: { message: 'Der Server hat eine unerwartete Adresse geliefert.' } }; await shell.openExternal(u.href); return { data: true }; } catch (e) { return { error: { 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, language: null }; let settings = { ...DEFAULTS }; // Sprache für Tray-Menü und Meldungen (Auswahl im Programm, sonst die von Windows) const lang = () => (settings.language === 'de' || settings.language === 'en' ? settings.language : ((app.getLocale() || 'de').toLowerCase().startsWith('de') ? 'de' : 'en')); const L = (de, en) => (lang() === 'en' ? en : de); 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(); try { tray?.setContextMenu(trayMenu()); } catch {} 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 trayMenu() { return Menu.buildFromTemplate([ { label: L('DeskHub öffnen', 'Open DeskHub'), click: () => showWindow() }, { label: 'Tickets', click: () => showWindow('tickets') }, { label: L('Neues Ticket', 'New ticket'), click: () => showWindow('newticket') }, { type: 'separator' }, { label: L('Beenden', 'Quit'), click: () => { quitting = true; app.quit(); } }, ]); } 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(trayMenu()); 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} ${L('neu', 'new')}` : ''); tray?.setToolTip(count > 0 ? `DeskHub – ${count} ${L('neu', 'new')}` : '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(); }); }