import { app, BrowserWindow, dialog, ipcMain } from 'electron'; import type { OpenDialogOptions } from 'electron'; import path from 'node:path'; import fs from 'node:fs'; import crypto from 'node:crypto'; import axios from 'axios'; type Nonce = { id?: string; _id?: string; nonce: string; expiresAt?: string }; let mainWindow: BrowserWindow | null = null; let privateKey: Buffer | null = null; let privateKeyPath: string | null = null; const apiHost = (process.env.API_HOST || 'https://api.turnosxpress.com.ar').replace(/\/+$/, ''); const adminRoutes: Record = { 'users/paginate': { path: '/sysadmin/users/paginate', timeout: 5000 }, 'users/update': { path: '/sysadmin/users/update', timeout: 5000 }, 'users/set-verified': { path: '/sysadmin/users/set-verified', timeout: 5000 }, 'users/delete': { path: '/sysadmin/users/delete', timeout: 10000 }, 'users/organizations-status': { path: '/sysadmin/users/organizations-status', timeout: 5000 }, 'users/recalculate-plan-usage-cycle': { path: '/sysadmin/plan-usage-cycle/recalculate-user', timeout: 10000 }, 'users/subscription-details': { path: '/sysadmin/subscriptions/user-details', timeout: 10000 }, 'users/extend-subscription': { path: '/sysadmin/subscriptions/extend-user', timeout: 10000 }, 'users/finalize-subscription': { path: '/sysadmin/subscriptions/finalize-user', timeout: 10000 }, 'plans/list': { path: '/sysadmin/plans/list', timeout: 5000 }, 'plans/update': { path: '/sysadmin/plans/update', timeout: 5000 }, 'wap/paginate': { path: '/sysadmin/wapserver/get', timeout: 5000 }, 'wap/audit': { path: '/sysadmin/wapserver/audit', timeout: 10000 }, 'wap/create': { path: '/sysadmin/wapserver/create', timeout: 5000 }, 'wap/update': { path: '/sysadmin/wapserver/update', timeout: 5000 }, 'wap/recalculate-count': { path: '/sysadmin/wapserver/recalculate-count', timeout: 5000 }, 'wap/delete': { path: '/sysadmin/wapserver/delete', timeout: 15000 }, 'wap/bot/start': { path: '/sysadmin/wapserver/bot/start', timeout: 10000 }, 'wap/bot/stop': { path: '/sysadmin/wapserver/bot/stop', timeout: 10000 }, 'wap/bot/restart': { path: '/sysadmin/wapserver/bot/restart', timeout: 15000 }, 'wap/bot/qr': { path: '/sysadmin/wapserver/bot/qr', timeout: 10000 }, 'wap/bot/delete': { path: '/sysadmin/wapserver/bot/delete', timeout: 10000 }, 'wap/bot/detach': { path: '/sysadmin/wapserver/bot/detach', timeout: 10000 }, 'companies/paginate': { path: '/sysadmin/companies/paginate' }, 'companies/update': { path: '/sysadmin/companies/update' }, 'companies/set-banned': { path: '/sysadmin/companies/set-banned' }, 'companies/insights': { path: '/sysadmin/companies/insights' }, 'services/paginate': { path: '/sysadmin/services/paginate' }, 'services/set-banned': { path: '/sysadmin/services/set-banned' }, }; const createWindow = () => { mainWindow = new BrowserWindow({ width: 1280, height: 820, minWidth: 980, minHeight: 640, frame: false, titleBarStyle: 'hidden', backgroundColor: '#0f172a', icon: path.join(__dirname, '../build/icon.png'), webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, }); if (process.env.VITE_DEV_SERVER_URL) { void mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL); } else { void mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); } }; const loadPrivateKey = async (filePath: string) => { const key = await fs.promises.readFile(filePath); crypto.createPrivateKey(key); privateKey = key; privateKeyPath = filePath; }; const createPayload = async () => { if (!privateKey) { throw new Error('Load a private key before using admin functions.'); } const nonceResponse = await axios.get(`${apiHost}/sysadmin-challenges/create`, { headers: { 'Content-Type': 'application/json' }, timeout: 5000, }); if (nonceResponse.status !== 201 || !nonceResponse.data?.nonce) { throw new Error('Could not create the sysadmin nonce.'); } const signature = crypto.sign(null, Buffer.from(nonceResponse.data.nonce), privateKey); return { nonce: nonceResponse.data, signature: signature.toString('base64') }; }; const formatError = (error: unknown) => { if (axios.isAxiosError(error)) { const data = error.response?.data; const message = typeof data === 'string' ? data : data?.desc || data?.message || data?.error || error.message; return { message, status: error.response?.status, data }; } return { message: error instanceof Error ? error.message : 'Unknown error' }; }; ipcMain.handle('key:status', () => ({ loaded: Boolean(privateKey), path: privateKeyPath })); ipcMain.handle('window:minimize', () => mainWindow?.minimize()); ipcMain.handle('window:toggle-maximize', () => { if (!mainWindow) return false; if (mainWindow.isMaximized()) { mainWindow.unmaximize(); return false; } mainWindow.maximize(); return true; }); ipcMain.handle('window:close', () => mainWindow?.close()); ipcMain.handle('key:select', async () => { const dialogOptions: OpenDialogOptions = { title: 'Select sysadmin private key', properties: ['openFile'], filters: [{ name: 'Private key', extensions: ['key', 'pem', '*'] }], }; const result = mainWindow ? await dialog.showOpenDialog(mainWindow, dialogOptions) : await dialog.showOpenDialog(dialogOptions); if (result.canceled || !result.filePaths[0]) { return { loaded: Boolean(privateKey), path: privateKeyPath }; } await loadPrivateKey(result.filePaths[0]); return { loaded: true, path: privateKeyPath }; }); ipcMain.handle('admin:request', async (_event, routeName: string, body: unknown) => { if (!privateKey) { throw new Error('Load a private key before using admin functions.'); } const route = adminRoutes[routeName]; if (!route) { throw new Error(`Unsupported admin route: ${routeName}`); } try { const payload = await createPayload(); const response = await axios.post(`${apiHost}${route.path}`, { ...(body as object), payload }, { headers: { 'Content-Type': 'application/json' }, timeout: route.timeout ?? 5000, }); return response.data; } catch (error) { throw new Error(JSON.stringify(formatError(error))); } }); void app.whenReady().then(createWindow); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });