127 lines
4.2 KiB
TypeScript

import { app, BrowserWindow } from 'electron'
import path from 'node:path'
import started from 'electron-squirrel-startup'
import './ipcHandlers'
import logger from './logger'
const isDev = !app.isPackaged
if (started) {
app.quit()
}
// Security: Handle certificate errors - reject invalid certificates to prevent phishing
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
if (isDev && (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1'))) {
event.preventDefault()
callback(true)
return
}
logger.warn(`Certificate error for ${url}: ${error}`)
event.preventDefault()
callback(false)
})
const createWindow = () => {
logger.info('App started')
const mainWindow = new BrowserWindow({
width: 800,
height: 800,
minWidth: 500,
minHeight: 650,
icon: path.resolve(process.cwd(), 'public/favicon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
devTools: isDev,
sandbox: true,
},
})
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else {
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`))
}
// Security: Prevent opening new windows - block all window.open() calls
mainWindow.webContents.setWindowOpenHandler(() => {
logger.warn('Blocked attempt to open new window')
return { action: 'deny' }
})
// Security: Control file downloads - validate and log all downloads
mainWindow.webContents.session.on('will-download', (event, item) => {
const fileName = item.getFilename()
const totalBytes = item.getTotalBytes()
logger.info(`Download started: ${fileName} (${totalBytes} bytes)`)
// Validate file extension - only allow safe file types
const allowedExtensions = ['.json', '.txt', '.csv']
const fileExt = path.extname(fileName).toLowerCase()
if (!allowedExtensions.includes(fileExt)) {
logger.warn(`Blocked download of potentially unsafe file: ${fileName}`)
item.cancel()
return
}
// Log download progress
item.on('updated', () => {
logger.info(
`Download progress: ${fileName} - ${item.getReceivedBytes()}/${totalBytes} bytes`
)
})
item.on('done', (event, state) => {
if (state === 'completed') {
logger.info(`Download completed: ${fileName}`)
} else if (state === 'cancelled') {
logger.warn(`Download cancelled: ${fileName}`)
} else {
logger.error(`Download interrupted: ${fileName} - ${state}`)
}
})
})
if (!isDev) {
mainWindow.removeMenu()
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
event.preventDefault()
}
})
}
mainWindow.webContents.on('devtools-opened', () => {
if (!isDev) mainWindow.webContents.closeDevTools()
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
logger.info('App closed')
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.