692 lines
23 KiB
JavaScript
692 lines
23 KiB
JavaScript
const { app, BrowserWindow, Tray, Menu, ipcMain, shell, dialog, nativeImage } = require('electron');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { fork } = require('child_process');
|
|
const { autoUpdater } = require('electron-updater');
|
|
|
|
let tray = null;
|
|
let configWindow = null;
|
|
let monitorProcess = null;
|
|
let currentStatus = { connected: false, identified: false };
|
|
let downloadProgress = null; // Variável para controlar o progresso de download
|
|
const recentLogs = [];
|
|
|
|
// ================================================================
|
|
// CONTROLE DE PAUSA DE ENVIO
|
|
// ================================================================
|
|
let isSendingPaused = false; // true = envio de imagens pausado
|
|
let autoResumeTimer = null; // timer que retoma o envio sozinho
|
|
const AUTO_RESUME_MS = 10 * 60 * 1000; // 10 minutos
|
|
|
|
function pauseSending() {
|
|
isSendingPaused = true;
|
|
if (monitorProcess) monitorProcess.send({ command: 'pause' });
|
|
|
|
// Agenda retomada automática após 10 min, caso o usuário esqueça de continuar
|
|
if (autoResumeTimer) clearTimeout(autoResumeTimer);
|
|
autoResumeTimer = setTimeout(() => {
|
|
addLog('▶️ Auto-retomada: 10 minutos se passaram, retomando envio automaticamente.');
|
|
resumeSending(true);
|
|
}, AUTO_RESUME_MS);
|
|
|
|
if (tray) {
|
|
tray.displayBalloon({
|
|
title: 'ScoreOdonto - Envio Pausado',
|
|
content: 'O envio de imagens foi pausado. Ele será retomado automaticamente em 10 minutos se você não continuar antes.',
|
|
iconType: 'warning'
|
|
});
|
|
}
|
|
updateTrayMenu();
|
|
}
|
|
|
|
function resumeSending(isAuto = false) {
|
|
isSendingPaused = false;
|
|
if (autoResumeTimer) { clearTimeout(autoResumeTimer); autoResumeTimer = null; }
|
|
if (monitorProcess) monitorProcess.send({ command: 'resume' });
|
|
|
|
if (tray && !isAuto) {
|
|
tray.displayBalloon({
|
|
title: 'ScoreOdonto - Envio Retomado',
|
|
content: 'O envio de imagens foi retomado.',
|
|
iconType: 'info'
|
|
});
|
|
}
|
|
updateTrayMenu();
|
|
}
|
|
|
|
function toggleSending() {
|
|
if (isSendingPaused) resumeSending(false);
|
|
else pauseSending();
|
|
}
|
|
|
|
// ================================================================
|
|
// AUTO-UPDATER CONFIG
|
|
// ================================================================
|
|
autoUpdater.autoDownload = true;
|
|
autoUpdater.autoInstallOnAppQuit = true;
|
|
autoUpdater.logger = require('electron').app ? console : null;
|
|
|
|
// ---- Trava anti-loop de atualização --------------------------------
|
|
// Se uma instalação falha (ex.: install incompleto), o app reabre na MESMA
|
|
// versão, detecta o update de novo e reentra em loop. Aqui contamos quantas
|
|
// vezes tentamos instalar cada versão; após o limite, paramos de auto-instalar
|
|
// e avisamos o usuário para reinstalar manualmente — quebrando o loop.
|
|
const MAX_UPDATE_ATTEMPTS = 3;
|
|
const updateStatePath = () => path.join(app.getPath('userData'), 'update-state.json');
|
|
|
|
function readUpdateState() {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(updateStatePath(), 'utf8')) || {};
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function writeUpdateState(state) {
|
|
try {
|
|
fs.writeFileSync(updateStatePath(), JSON.stringify(state, null, 2), 'utf8');
|
|
} catch (e) {
|
|
console.error('Erro ao gravar update-state.json:', e.message);
|
|
}
|
|
}
|
|
|
|
// Chamado no boot: se a versão atual já alcançou (ou passou) a versão tentada,
|
|
// a instalação teve sucesso → zera o contador.
|
|
function reconcileUpdateState() {
|
|
const state = readUpdateState();
|
|
if (state.version && state.version === app.getVersion()) {
|
|
console.log(`✅ Atualização para v${state.version} concluída — limpando contador.`);
|
|
writeUpdateState({});
|
|
}
|
|
}
|
|
|
|
let lastUpdaterErrorAt = 0;
|
|
function notifyUpdaterError(message) {
|
|
const now = Date.now();
|
|
// Throttle: no máximo 1 balão a cada 60s para não inundar o usuário
|
|
if (now - lastUpdaterErrorAt < 60000) return;
|
|
lastUpdaterErrorAt = now;
|
|
if (tray) {
|
|
tray.displayBalloon({
|
|
title: 'ScoreOdonto - Falha na Atualização',
|
|
content: message,
|
|
iconType: 'error'
|
|
});
|
|
}
|
|
}
|
|
|
|
autoUpdater.on('checking-for-update', () => {
|
|
console.log('🔍 Verificando atualizações...');
|
|
});
|
|
|
|
autoUpdater.on('update-available', (info) => {
|
|
console.log('🔄 Nova versão disponível:', info.version);
|
|
downloadProgress = 0; // Inicializa o progresso
|
|
updateTrayMenu(); // Atualiza o menu para mostrar a aba "Baixando..."
|
|
if (tray) {
|
|
tray.displayBalloon({
|
|
title: 'Nova Atualização Encontrada',
|
|
content: `A versão ${info.version} está sendo baixada. Clique com botão direito no ícone para acompanhar o progresso!`,
|
|
iconType: 'info'
|
|
});
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('update-not-available', () => {
|
|
console.log('✅ Aplicativo está na versão mais recente.');
|
|
});
|
|
|
|
autoUpdater.on('download-progress', (progress) => {
|
|
const p = Math.round(progress.percent);
|
|
console.log(`⬇️ Baixando atualização: ${p}%`);
|
|
if (downloadProgress !== p) {
|
|
downloadProgress = p;
|
|
// Atualiza a bandeja com throttling para não piscar demais
|
|
if (p % 5 === 0 || p === 100) {
|
|
updateTrayMenu();
|
|
}
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('update-downloaded', (info) => {
|
|
console.log('✅ Atualização baixada:', info.version);
|
|
|
|
// --- Trava anti-loop: não reinstalar a mesma versão indefinidamente ---
|
|
const state = readUpdateState();
|
|
const attempts = (state.version === info.version ? state.attempts : 0) || 0;
|
|
|
|
if (attempts >= MAX_UPDATE_ATTEMPTS) {
|
|
console.warn(`🛑 v${info.version} já falhou ${attempts}x — abortando auto-instalação para evitar loop.`);
|
|
downloadProgress = null;
|
|
updateTrayMenu();
|
|
notifyUpdaterError(
|
|
`A atualização para a versão ${info.version} falhou ${attempts} vezes e foi interrompida. ` +
|
|
`Por favor, baixe e instale o cliente manualmente para concluir.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Registra mais uma tentativa ANTES de reiniciar; será zerado no próximo
|
|
// boot se a versão tiver realmente avançado (reconcileUpdateState).
|
|
writeUpdateState({ version: info.version, attempts: attempts + 1 });
|
|
|
|
console.log(`↻ Reiniciando em 15s para instalar v${info.version} (tentativa ${attempts + 1}/${MAX_UPDATE_ATTEMPTS})...`);
|
|
downloadProgress = "Pronto";
|
|
|
|
if (tray) {
|
|
tray.displayBalloon({
|
|
title: 'ScoreOdonto - Atualização Pronta',
|
|
content: `Versão ${info.version} baixada. O aplicativo será reiniciado automaticamente em 15 segundos.`,
|
|
iconType: 'info'
|
|
});
|
|
}
|
|
|
|
// Contagem regressiva no tray tooltip
|
|
let countdown = 15;
|
|
const countdownInterval = setInterval(() => {
|
|
countdown--;
|
|
if (tray) tray.setToolTip(`ScoreOdonto — Reiniciando em ${countdown}s para instalar v${info.version}...`);
|
|
if (countdown <= 0) {
|
|
clearInterval(countdownInterval);
|
|
// isSilent=true (sem janela do instalador), isForceRunAfter=true (reabre após instalar)
|
|
autoUpdater.quitAndInstall(true, true);
|
|
}
|
|
}, 1000);
|
|
|
|
// Permite cancelar o restart manual pelo menu
|
|
updateTrayMenu(countdown, countdownInterval, info.version);
|
|
});
|
|
|
|
autoUpdater.on('error', (err) => {
|
|
console.error('❌ Erro no auto-updater:', err.message);
|
|
downloadProgress = null;
|
|
updateTrayMenu();
|
|
notifyUpdaterError(`Não foi possível atualizar: ${err.message}`);
|
|
});
|
|
|
|
const appDataFolder = app.getPath('userData');
|
|
const configPath = path.join(appDataFolder, 'config.json');
|
|
const configBackupPath = path.join(appDataFolder, 'config.backup.json');
|
|
|
|
// Ensure directory exists
|
|
if (!fs.existsSync(appDataFolder)) {
|
|
fs.mkdirSync(appDataFolder, { recursive: true });
|
|
}
|
|
|
|
// Default config template
|
|
const defaultConfig = {
|
|
serverUrl: "https://rx.scoreodonto.com",
|
|
monitorPath: "C:\\ProgramData\\RF\\Dental Sensor\\Images",
|
|
clientName: "DIGITE-O-NOME-DA-MAQUINA",
|
|
clientType: "windows",
|
|
apiKey: "rf-dental-secure-key-2026",
|
|
serverEmail: "",
|
|
serverPassword: "",
|
|
token: "",
|
|
adminUsername: "admin",
|
|
adminPassword: "admin"
|
|
};
|
|
|
|
// Lê e valida um JSON de config; retorna null se ausente/corrompido
|
|
function readConfigFile(filePath) {
|
|
try {
|
|
if (!fs.existsSync(filePath)) return null;
|
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
// Só consideramos válido se for um objeto com ao menos um campo
|
|
if (parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0) {
|
|
return parsed;
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
console.error(`Error reading config file ${filePath}:`, e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Mantém uma cópia de segurança do config para não perder clientId/credenciais
|
|
// em caso de corrupção (evita reconfiguração do cliente do zero).
|
|
function writeConfigBackup(cfg) {
|
|
try {
|
|
fs.writeFileSync(configBackupPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
} catch (e) {
|
|
console.error("Error writing config backup:", e.message);
|
|
}
|
|
}
|
|
|
|
function loadConfig() {
|
|
// 1. Tenta o arquivo principal
|
|
let userConfig = readConfigFile(configPath);
|
|
|
|
// 2. Se falhar, tenta restaurar do backup (rede de segurança)
|
|
if (!userConfig) {
|
|
const backup = readConfigFile(configBackupPath);
|
|
if (backup) {
|
|
console.warn("⚠️ config.json ausente/corrompido — restaurando a partir de config.backup.json");
|
|
userConfig = backup;
|
|
try {
|
|
fs.writeFileSync(configPath, JSON.stringify(backup, null, 2), 'utf8');
|
|
} catch (e) {
|
|
console.error("Falha ao restaurar config.json do backup:", e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Sem principal nem backup: primeira execução → grava template padrão
|
|
if (!userConfig) {
|
|
try {
|
|
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
|
} catch (e) {
|
|
console.error("Error writing default config:", e.message);
|
|
}
|
|
return defaultConfig;
|
|
}
|
|
|
|
const merged = { ...defaultConfig, ...userConfig };
|
|
// Mantém o backup atualizado sempre que carregamos um config válido
|
|
writeConfigBackup(merged);
|
|
return merged;
|
|
}
|
|
|
|
function saveConfig(newConfig) {
|
|
try {
|
|
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
|
// Atualiza o backup logo após gravar com sucesso
|
|
writeConfigBackup(newConfig);
|
|
return true;
|
|
} catch (e) {
|
|
console.error("Error saving config:", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Start background child process
|
|
function startMonitorProcess() {
|
|
if (monitorProcess) {
|
|
monitorProcess.kill();
|
|
}
|
|
|
|
const config = loadConfig();
|
|
// Don't start if it's default/placeholder
|
|
if (!config.clientName || config.clientName === defaultConfig.clientName) {
|
|
console.log("Monitor process not started: clientName not configured.");
|
|
return;
|
|
}
|
|
|
|
console.log("Starting client-monitor.js as background process...");
|
|
monitorProcess = fork(path.join(__dirname, 'client-monitor.js'), [], {
|
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
env: {
|
|
...process.env,
|
|
DENTAL_CONFIG_DIR: appDataFolder
|
|
}
|
|
});
|
|
|
|
monitorProcess.on('message', (message) => {
|
|
if (message && message.event === 'status') {
|
|
currentStatus.connected = message.connected;
|
|
currentStatus.identified = message.identified;
|
|
updateTrayMenu();
|
|
} else if (message && message.event === 'log') {
|
|
recentLogs.push(message.log);
|
|
if (recentLogs.length > 100) recentLogs.shift();
|
|
|
|
if (configWindow && !configWindow.isDestroyed()) {
|
|
configWindow.webContents.send('new-log', message.log);
|
|
}
|
|
} else if (message && message.event === 'resize-image') {
|
|
try {
|
|
const img = nativeImage.createFromBuffer(Buffer.from(message.base64, 'base64'));
|
|
const thumb = img.resize({ width: 300, quality: "good" }).toJPEG(80);
|
|
monitorProcess.send({ event: 'resize-result', id: message.id, success: true, thumbBase64: thumb.toString('base64') });
|
|
} catch (e) {
|
|
monitorProcess.send({ event: 'resize-result', id: message.id, success: false, error: e.message });
|
|
}
|
|
} else if (message && message.event === 'wasabi-error') {
|
|
if (tray) {
|
|
tray.displayBalloon({
|
|
title: 'ScoreOdonto — Falha no Armazenamento',
|
|
content: message.message || 'Erro ao comunicar com o Wasabi.',
|
|
iconType: 'error'
|
|
});
|
|
}
|
|
} else if (message && message.event === 'reupload-requested') {
|
|
if (tray) {
|
|
tray.displayBalloon({
|
|
title: 'ScoreOdonto — Reenvio em Andamento',
|
|
content: `O servidor solicitou o reenvio de ${message.count} imagem(ns) ausente(s) no armazenamento. Reenviando automaticamente...`,
|
|
iconType: 'info'
|
|
});
|
|
}
|
|
} else if (message && message.event === 'force-update') {
|
|
autoUpdater.checkForUpdatesAndNotify();
|
|
}
|
|
});
|
|
|
|
monitorProcess.on('exit', (code) => {
|
|
console.log(`Monitor process exited with code ${code}`);
|
|
currentStatus.connected = false;
|
|
currentStatus.identified = false;
|
|
updateTrayMenu();
|
|
});
|
|
|
|
// Se o envio estava pausado, reaplica o estado no monitor recém-iniciado
|
|
if (isSendingPaused) {
|
|
setTimeout(() => {
|
|
if (monitorProcess) monitorProcess.send({ command: 'pause' });
|
|
}, 2500);
|
|
}
|
|
}
|
|
|
|
function createConfigWindow() {
|
|
if (configWindow) {
|
|
configWindow.focus();
|
|
return;
|
|
}
|
|
|
|
configWindow = new BrowserWindow({
|
|
width: 600,
|
|
height: 680,
|
|
minHeight: 520,
|
|
title: "ScoreOdonto Dental Client - Configuração",
|
|
icon: path.join(__dirname, 'favicon.png'),
|
|
resizable: true,
|
|
maximizable: true,
|
|
autoHideMenuBar: true,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false
|
|
}
|
|
});
|
|
|
|
configWindow.loadFile(path.join(__dirname, 'config-ui.html'));
|
|
|
|
configWindow.on('closed', () => {
|
|
configWindow = null;
|
|
});
|
|
}
|
|
|
|
let pendingCountdownInterval = null;
|
|
|
|
function updateTrayMenu(countdown = null, countdownInterval = null, updateVersion = null) {
|
|
if (!tray) return;
|
|
|
|
// Salva o interval ativo para poder cancelar
|
|
if (countdownInterval !== null) pendingCountdownInterval = countdownInterval;
|
|
|
|
let statusText = "ScoreOdonto: Desconectado 🔴";
|
|
if (currentStatus.connected) {
|
|
statusText = currentStatus.identified
|
|
? "ScoreOdonto: Identificado 🟢"
|
|
: "ScoreOdonto: Conectado... 🟡";
|
|
}
|
|
|
|
const template = [
|
|
{ label: statusText, enabled: false },
|
|
{ type: 'separator' }
|
|
];
|
|
|
|
// Se houver um download em andamento, mostra a barra no menu
|
|
if (downloadProgress !== null) {
|
|
if (downloadProgress === "Pronto") {
|
|
const countdownLabel = countdown !== null
|
|
? `⏱️ Reiniciando em ${countdown}s para instalar v${updateVersion}...`
|
|
: '📦 Atualização pronta para instalar';
|
|
template.push({ label: countdownLabel, enabled: false });
|
|
template.push({
|
|
label: '🚀 Instalar Agora (Reiniciar)',
|
|
click: () => {
|
|
if (pendingCountdownInterval) clearInterval(pendingCountdownInterval);
|
|
autoUpdater.quitAndInstall(true, true);
|
|
}
|
|
});
|
|
} else {
|
|
template.push({ label: `⬇️ Baixando atualização: ${downloadProgress}%`, enabled: false });
|
|
}
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
template.push(
|
|
{
|
|
label: isSendingPaused ? '⏸️ Envio PAUSADO (retoma em ~10 min)' : '▶️ Envio Ativo',
|
|
enabled: false
|
|
},
|
|
{
|
|
label: 'Pausar Envio de Imagens',
|
|
type: 'checkbox',
|
|
checked: isSendingPaused,
|
|
click: () => toggleSending()
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: 'Forçar Sincronização 🔄',
|
|
click: () => {
|
|
if (isSendingPaused) {
|
|
resumeSending(false);
|
|
}
|
|
if (monitorProcess) {
|
|
monitorProcess.send({ command: 'sync' });
|
|
} else {
|
|
startMonitorProcess();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
label: 'Verificar Atualizações 🔄',
|
|
click: () => {
|
|
autoUpdater.checkForUpdatesAndNotify();
|
|
}
|
|
},
|
|
{
|
|
label: 'Configurações ⚙️',
|
|
click: () => {
|
|
createConfigWindow();
|
|
}
|
|
},
|
|
{
|
|
label: 'Abrir Pasta de Imagens 📂',
|
|
click: () => {
|
|
const config = loadConfig();
|
|
if (fs.existsSync(config.monitorPath)) {
|
|
shell.openPath(config.monitorPath);
|
|
} else {
|
|
shell.openPath(path.dirname(config.monitorPath));
|
|
}
|
|
}
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: 'Sair 🚪',
|
|
click: () => {
|
|
app.isQuitting = true;
|
|
app.quit();
|
|
}
|
|
}
|
|
);
|
|
|
|
const contextMenu = Menu.buildFromTemplate(template);
|
|
tray.setContextMenu(contextMenu);
|
|
tray.setToolTip(`ScoreOdonto Dental Client\n${statusText}`);
|
|
}
|
|
|
|
function initTray() {
|
|
const iconPath = path.join(__dirname, 'favicon.png');
|
|
tray = new Tray(iconPath);
|
|
updateTrayMenu();
|
|
|
|
tray.on('double-click', () => {
|
|
createConfigWindow();
|
|
});
|
|
}
|
|
|
|
// Single instance lock
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
if (!gotTheLock) {
|
|
app.quit();
|
|
return;
|
|
}
|
|
|
|
app.on('second-instance', () => {
|
|
if (configWindow) {
|
|
if (configWindow.isMinimized()) configWindow.restore();
|
|
configWindow.focus();
|
|
} else {
|
|
createConfigWindow();
|
|
}
|
|
});
|
|
|
|
app.whenReady().then(() => {
|
|
initTray();
|
|
|
|
// Se a versão atual já é a que tentávamos instalar, a atualização teve
|
|
// sucesso → zera o contador anti-loop antes de checar novamente.
|
|
reconcileUpdateState();
|
|
|
|
// Verificar atualizações silenciosamente ao iniciar
|
|
autoUpdater.checkForUpdatesAndNotify();
|
|
|
|
const config = loadConfig();
|
|
// If not configured, show config UI
|
|
if (!config.clientName || config.clientName === defaultConfig.clientName) {
|
|
createConfigWindow();
|
|
} else {
|
|
// Start background process
|
|
startMonitorProcess();
|
|
}
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
// Keep running in tray on close
|
|
if (process.platform !== 'darwin') {
|
|
// do nothing, let it run in tray
|
|
}
|
|
});
|
|
|
|
app.on('before-quit', () => {
|
|
if (monitorProcess) {
|
|
monitorProcess.kill();
|
|
}
|
|
});
|
|
|
|
// IPC communication
|
|
ipcMain.handle('get-config', () => {
|
|
return loadConfig();
|
|
});
|
|
|
|
ipcMain.handle('get-version', () => {
|
|
return app.getVersion();
|
|
});
|
|
|
|
ipcMain.handle('get-logs', () => {
|
|
return recentLogs;
|
|
});
|
|
|
|
ipcMain.handle('save-config', (event, newConfig) => {
|
|
const success = saveConfig(newConfig);
|
|
if (success) {
|
|
// Restart child process with new settings
|
|
startMonitorProcess();
|
|
}
|
|
return success;
|
|
});
|
|
|
|
ipcMain.handle('test-server', async (event, url) => {
|
|
return new Promise((resolve) => {
|
|
try {
|
|
const checkUrl = url.endsWith('/') ? `${url}status` : `${url}/status`;
|
|
const clientModule = checkUrl.startsWith('https') ? require('https') : require('http');
|
|
const req = clientModule.get(checkUrl, { timeout: 4000 }, (res) => {
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
resolve({ success: true, message: `Conexão bem sucedida! Código: ${res.statusCode}` });
|
|
} else {
|
|
resolve({ success: false, error: `Código de status inesperado: ${res.statusCode}` });
|
|
}
|
|
});
|
|
req.on('error', (err) => {
|
|
resolve({ success: false, error: err.message });
|
|
});
|
|
req.on('timeout', () => {
|
|
req.destroy();
|
|
resolve({ success: false, error: 'Timeout de 4 segundos esgotado.' });
|
|
});
|
|
} catch (e) {
|
|
resolve({ success: false, error: e.message });
|
|
}
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('verify-credentials', async (event, url, email, password, token) => {
|
|
return new Promise((resolve) => {
|
|
try {
|
|
let base = url.trim();
|
|
if (base.endsWith('/')) {
|
|
base = base.slice(0, -1);
|
|
}
|
|
const verifyUrl = `${base}/api/auth/verify-client-credentials`;
|
|
const clientModule = verifyUrl.startsWith('https') ? require('https') : require('http');
|
|
const parsedUrl = new URL(verifyUrl);
|
|
|
|
const postData = JSON.stringify({ email, password, token });
|
|
|
|
const options = {
|
|
hostname: parsedUrl.hostname,
|
|
port: parsedUrl.port || (verifyUrl.startsWith('https') ? 443 : 80),
|
|
path: parsedUrl.pathname,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(postData)
|
|
},
|
|
timeout: 8000
|
|
};
|
|
|
|
const req = clientModule.request(options, (res) => {
|
|
let body = '';
|
|
res.on('data', (chunk) => body += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
if (res.statusCode >= 200 && res.statusCode < 300 && data.success) {
|
|
resolve({ success: true, clinic: data.clinic });
|
|
} else {
|
|
resolve({ success: false, error: data.error || `Erro de validação: ${res.statusCode}` });
|
|
}
|
|
} catch (e) {
|
|
resolve({ success: false, error: `Resposta inválida do servidor: ${e.message}` });
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', (err) => {
|
|
resolve({ success: false, error: `Falha de rede: ${err.message}` });
|
|
});
|
|
|
|
req.on('timeout', () => {
|
|
req.destroy();
|
|
resolve({ success: false, error: 'Tempo limite esgotado ao tentar validar credenciais (8s).' });
|
|
});
|
|
|
|
req.write(postData);
|
|
req.end();
|
|
} catch (e) {
|
|
resolve({ success: false, error: `Erro interno: ${e.message}` });
|
|
}
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('open-folder', () => {
|
|
const config = loadConfig();
|
|
const targetPath = config.monitorPath;
|
|
if (!fs.existsSync(targetPath)) {
|
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
}
|
|
shell.openPath(targetPath);
|
|
return true;
|
|
});
|
|
|
|
ipcMain.on('close-window', () => {
|
|
if (configWindow) {
|
|
configWindow.close();
|
|
}
|
|
});
|