mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
* feat(electron): add desktop application support with electron - implement complete Electron main process architecture with window management, app menu, IPC handlers, and settings window - integrate Next.js server for production builds with embedded standalone server - add configuration management with persistent storage and env file support - create preload scripts with secure context bridge for renderer communication - set up electron-builder configuration for multi-platform packaging (macOS, Windows, Linux) - add GitHub Actions workflow for automated release builds - include development scripts for hot-reload during Electron development * feat(electron): enhance security and stability - encrypt API keys using Electron safeStorage API before persisting to disk - add error handling and rollback for preset switching failures - extract inline styles to external CSS file and remove unsafe-inline from CSP - implement dynamic port allocation with automatic fallback for production builds * fix(electron): add maintainer field for Linux .deb package - add maintainer email to linux configuration in electron-builder.yml - required for building .deb packages * fix(electron): use shx for cross-platform file copying - replace Unix-only cp -r with npx shx cp -r - add shx as devDependency for Windows compatibility * fix(electron): fix runtime icon path for all platforms - use icon.png directly instead of platform-specific formats - electron-builder handles icon conversion during packaging - macOS uses embedded icon from app bundle, no explicit path needed - add icon.png to extraResources for Windows/Linux runtime access * fix(electron): add security warning for plaintext API key storage - warn user when safeStorage is unavailable (Linux without keyring) - fail secure: throw error if encryption fails instead of storing plaintext - prevent duplicate warnings with hasWarnedAboutPlaintext flag * fix(electron): add remaining review fixes - Add Windows ARM64 architecture support - Add IPC input validation with config key whitelist - Add server.js existence check before starting Next.js server - Make afterPack throw error on missing directories - Add workflow permissions for release job --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
213 lines
6.0 KiB
TypeScript
213 lines
6.0 KiB
TypeScript
import { app, BrowserWindow, dialog, ipcMain } from "electron"
|
|
import {
|
|
applyPresetToEnv,
|
|
type ConfigPreset,
|
|
createPreset,
|
|
deletePreset,
|
|
getAllPresets,
|
|
getCurrentPreset,
|
|
getCurrentPresetId,
|
|
setCurrentPreset,
|
|
updatePreset,
|
|
} from "./config-manager"
|
|
import { restartNextServer } from "./next-server"
|
|
|
|
/**
|
|
* Allowed configuration keys for presets
|
|
* This whitelist prevents arbitrary environment variable injection
|
|
*/
|
|
const ALLOWED_CONFIG_KEYS = new Set([
|
|
"AI_PROVIDER",
|
|
"AI_MODEL",
|
|
"AI_API_KEY",
|
|
"AI_BASE_URL",
|
|
"TEMPERATURE",
|
|
])
|
|
|
|
/**
|
|
* Sanitize preset config to only include allowed keys
|
|
*/
|
|
function sanitizePresetConfig(
|
|
config: Record<string, string | undefined>,
|
|
): Record<string, string | undefined> {
|
|
const sanitized: Record<string, string | undefined> = {}
|
|
for (const key of ALLOWED_CONFIG_KEYS) {
|
|
if (key in config && typeof config[key] === "string") {
|
|
sanitized[key] = config[key]
|
|
}
|
|
}
|
|
return sanitized
|
|
}
|
|
|
|
/**
|
|
* Register all IPC handlers
|
|
*/
|
|
export function registerIpcHandlers(): void {
|
|
// ==================== App Info ====================
|
|
|
|
ipcMain.handle("get-version", () => {
|
|
return app.getVersion()
|
|
})
|
|
|
|
// ==================== Window Controls ====================
|
|
|
|
ipcMain.on("window-minimize", (event) => {
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
|
win?.minimize()
|
|
})
|
|
|
|
ipcMain.on("window-maximize", (event) => {
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
|
if (win?.isMaximized()) {
|
|
win.unmaximize()
|
|
} else {
|
|
win?.maximize()
|
|
}
|
|
})
|
|
|
|
ipcMain.on("window-close", (event) => {
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
|
win?.close()
|
|
})
|
|
|
|
// ==================== File Dialogs ====================
|
|
|
|
ipcMain.handle("dialog-open-file", async (event) => {
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
|
if (!win) return null
|
|
|
|
const result = await dialog.showOpenDialog(win, {
|
|
properties: ["openFile"],
|
|
filters: [
|
|
{ name: "Draw.io Files", extensions: ["drawio", "xml"] },
|
|
{ name: "All Files", extensions: ["*"] },
|
|
],
|
|
})
|
|
|
|
if (result.canceled || result.filePaths.length === 0) {
|
|
return null
|
|
}
|
|
|
|
// Read the file content
|
|
const fs = await import("node:fs/promises")
|
|
try {
|
|
const content = await fs.readFile(result.filePaths[0], "utf-8")
|
|
return content
|
|
} catch (error) {
|
|
console.error("Failed to read file:", error)
|
|
return null
|
|
}
|
|
})
|
|
|
|
ipcMain.handle("dialog-save-file", async (event, data: string) => {
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
|
if (!win) return false
|
|
|
|
const result = await dialog.showSaveDialog(win, {
|
|
filters: [
|
|
{ name: "Draw.io Files", extensions: ["drawio"] },
|
|
{ name: "XML Files", extensions: ["xml"] },
|
|
],
|
|
})
|
|
|
|
if (result.canceled || !result.filePath) {
|
|
return false
|
|
}
|
|
|
|
const fs = await import("node:fs/promises")
|
|
try {
|
|
await fs.writeFile(result.filePath, data, "utf-8")
|
|
return true
|
|
} catch (error) {
|
|
console.error("Failed to save file:", error)
|
|
return false
|
|
}
|
|
})
|
|
|
|
// ==================== Config Presets ====================
|
|
|
|
ipcMain.handle("config-presets:get-all", () => {
|
|
return getAllPresets()
|
|
})
|
|
|
|
ipcMain.handle("config-presets:get-current", () => {
|
|
return getCurrentPreset()
|
|
})
|
|
|
|
ipcMain.handle("config-presets:get-current-id", () => {
|
|
return getCurrentPresetId()
|
|
})
|
|
|
|
ipcMain.handle(
|
|
"config-presets:save",
|
|
(
|
|
_event,
|
|
preset: Omit<ConfigPreset, "id" | "createdAt" | "updatedAt"> & {
|
|
id?: string
|
|
},
|
|
) => {
|
|
// Validate preset name
|
|
if (typeof preset.name !== "string" || !preset.name.trim()) {
|
|
throw new Error("Invalid preset name")
|
|
}
|
|
|
|
// Sanitize config to only allow whitelisted keys
|
|
const sanitizedConfig = sanitizePresetConfig(preset.config ?? {})
|
|
|
|
if (preset.id) {
|
|
// Update existing preset
|
|
return updatePreset(preset.id, {
|
|
name: preset.name.trim(),
|
|
config: sanitizedConfig,
|
|
})
|
|
}
|
|
// Create new preset
|
|
return createPreset({
|
|
name: preset.name.trim(),
|
|
config: sanitizedConfig,
|
|
})
|
|
},
|
|
)
|
|
|
|
ipcMain.handle("config-presets:delete", (_event, id: string) => {
|
|
return deletePreset(id)
|
|
})
|
|
|
|
ipcMain.handle("config-presets:apply", async (_event, id: string) => {
|
|
const env = applyPresetToEnv(id)
|
|
if (!env) {
|
|
return { success: false, error: "Preset not found" }
|
|
}
|
|
|
|
const isDev = process.env.NODE_ENV === "development"
|
|
|
|
if (isDev) {
|
|
// In development mode, the config file change will trigger
|
|
// the file watcher in electron-dev.mjs to restart Next.js
|
|
// We just need to save the preset (already done in applyPresetToEnv)
|
|
return { success: true, env, devMode: true }
|
|
}
|
|
|
|
// Production mode: restart the Next.js server to apply new environment variables
|
|
try {
|
|
await restartNextServer()
|
|
return { success: true, env }
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error:
|
|
error instanceof Error
|
|
? error.message
|
|
: "Failed to restart server",
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(
|
|
"config-presets:set-current",
|
|
(_event, id: string | null) => {
|
|
return setCurrentPreset(id)
|
|
},
|
|
)
|
|
}
|