feat: add TypeScript definitions and utility functions for date formatting

- 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.
This commit is contained in:
2026-08-14 19:13:32 -03:00
parent 9efb1792c7
commit 28295e346c
44 changed files with 10274 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
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();
});