feat(desktop): add secure electron shell with preload ipc and menu
This commit is contained in:
10
README.md
10
README.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
Electron desktop shell for Fabric.Frontend.
|
Electron desktop shell for Fabric.Frontend.
|
||||||
|
|
||||||
|
## 功能(当前)
|
||||||
|
- BrowserWindow 基础配置(尺寸/最小尺寸/标题)
|
||||||
|
- Dev/Prod 加载策略(dev server / 本地 offline.html)
|
||||||
|
- 基础菜单与快捷键(刷新、开发者工具、退出)
|
||||||
|
- 安全基线:`contextIsolation` + `sandbox` + 禁止任意新窗口/导航
|
||||||
|
- `preload + IPC` 白名单:
|
||||||
|
- `fabric:config:get`
|
||||||
|
- `fabric:config:set`
|
||||||
|
- `fabric:notify`
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
90
main.js
90
main.js
@@ -1,20 +1,104 @@
|
|||||||
const { app, BrowserWindow } = require('electron')
|
const { app, BrowserWindow, Menu, ipcMain, Notification, shell } = require('electron')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const isDev = !!process.env.FABRIC_DESKTOP_URL
|
||||||
|
const DEFAULT_DEV_URL = 'http://localhost:5173'
|
||||||
|
const DEFAULT_PROD_ENTRY = path.join(__dirname, 'offline.html')
|
||||||
|
|
||||||
|
function configPath() {
|
||||||
|
return path.join(app.getPath('userData'), 'fabric-desktop.config.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfig() {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(configPath(), 'utf8')
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
centerApiBase: 'http://localhost:7001/api',
|
||||||
|
guildApiBase: 'http://localhost:7002/api',
|
||||||
|
guildSocketBase: 'http://localhost:7002/realtime',
|
||||||
|
apiKey: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig(next) {
|
||||||
|
fs.mkdirSync(path.dirname(configPath()), { recursive: true })
|
||||||
|
fs.writeFileSync(configPath(), JSON.stringify(next, null, 2), 'utf8')
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
const template = [
|
||||||
|
{
|
||||||
|
label: 'Fabric',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'reload', accelerator: 'CmdOrCtrl+R' },
|
||||||
|
{ role: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'quit', accelerator: 'CmdOrCtrl+Q' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [{ role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'copy' }, { role: 'paste' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 840,
|
height: 840,
|
||||||
|
minWidth: 1024,
|
||||||
|
minHeight: 700,
|
||||||
|
title: 'Fabric Desktop',
|
||||||
|
autoHideMenuBar: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = process.env.FABRIC_DESKTOP_URL || 'file://' + __dirname + '/offline.html'
|
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
|
||||||
win.loadURL(url)
|
win.webContents.on('will-navigate', (event, url) => {
|
||||||
|
const allowedDev = isDev && url.startsWith((process.env.FABRIC_DESKTOP_URL || DEFAULT_DEV_URL))
|
||||||
|
const allowedFile = url.startsWith('file://')
|
||||||
|
if (!allowedDev && !allowedFile) {
|
||||||
|
event.preventDefault()
|
||||||
|
shell.openExternal(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const devUrl = process.env.FABRIC_DESKTOP_URL || DEFAULT_DEV_URL
|
||||||
|
if (isDev) {
|
||||||
|
win.loadURL(devUrl)
|
||||||
|
} else {
|
||||||
|
win.loadFile(DEFAULT_PROD_ENTRY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
createMenu()
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|
||||||
|
ipcMain.handle('fabric:config:get', () => readConfig())
|
||||||
|
ipcMain.handle('fabric:config:set', (_evt, next) => writeConfig(next || {}))
|
||||||
|
ipcMain.handle('fabric:notify', (_evt, payload) => {
|
||||||
|
const title = payload?.title || 'Fabric'
|
||||||
|
const body = payload?.body || ''
|
||||||
|
if (Notification.isSupported()) {
|
||||||
|
new Notification({ title, body }).show()
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
return { ok: false }
|
||||||
|
})
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
})
|
})
|
||||||
|
|||||||
7
preload.js
Normal file
7
preload.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron')
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('fabricDesktop', {
|
||||||
|
getConfig: () => ipcRenderer.invoke('fabric:config:get'),
|
||||||
|
setConfig: (next) => ipcRenderer.invoke('fabric:config:set', next),
|
||||||
|
notify: (title, body) => ipcRenderer.invoke('fabric:notify', { title, body }),
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user