28295e346c
- Created a global TypeScript declaration for the txadmin interface on the Window object. - Implemented date formatting utilities using date-fns with Spanish locale support. - Added TypeScript configuration files for app, Electron, and Node environments. - Set up Vite configuration for React application.
172 lines
6.4 KiB
TypeScript
172 lines
6.4 KiB
TypeScript
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<string, { path: string; timeout?: number }> = {
|
|
'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<Nonce>(`${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();
|
|
});
|