Upload via GUI (69 Dateien)

This commit is contained in:
2026-10-16 18:53:05 +00:00
parent 7d82ca66ac
commit 2e17e2ecb0
19 changed files with 3355 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
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}`;
}
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(); });
}