feat(electron): add main process, preload, store, protocol, and type definitions
- Add ElectronAPI type definitions and runtime contract (isElectron guard) - Add electron-store with backend connection data model - Add centralized API base URL utility (api-base.ts) - Implement app:// custom protocol with API proxy - Implement preload script with full contextBridge API - Complete main process: BrowserWindow config, IPC handlers, window controls - Register app:// scheme as privileged for secure renderer access
This commit is contained in:
89
dashboard/electron/main/protocol.ts
Normal file
89
dashboard/electron/main/protocol.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { net, protocol } from 'electron'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { dirname, extname, join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { getActiveBackend } from './store'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.mjs': 'application/javascript',
|
||||
'.cjs': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.txt': 'text/plain',
|
||||
'.webp': 'image/webp',
|
||||
}
|
||||
|
||||
export function registerAppProtocol(): void {
|
||||
protocol.handle('app', async (request) => {
|
||||
const url = new URL(request.url)
|
||||
const pathname = url.pathname
|
||||
|
||||
if (pathname.startsWith('/api/')) {
|
||||
const backend = getActiveBackend()
|
||||
const targetUrl = backend
|
||||
? `${backend.url.replace(/\/$/, '')}${pathname}${url.search}`
|
||||
: null
|
||||
|
||||
if (!targetUrl) {
|
||||
return new Response(JSON.stringify({ error: 'No backend configured' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const headers = new Headers(request.headers)
|
||||
headers.delete('host')
|
||||
|
||||
return net.fetch(targetUrl, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
|
||||
duplex: 'half',
|
||||
})
|
||||
}
|
||||
|
||||
// Dev mode: renderer is served by vite dev server, not app:// protocol
|
||||
if (process.env.ELECTRON_RENDERER_URL) {
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
|
||||
const rendererDir = join(__dirname, '../renderer')
|
||||
const safePath = decodeURIComponent(pathname)
|
||||
.replace(/\.\./g, '')
|
||||
.replace(/^\/+/, '')
|
||||
|
||||
const resolvedPath = safePath === '' ? 'index.html' : safePath
|
||||
const filePath = resolvedPath.endsWith('/')
|
||||
? join(rendererDir, resolvedPath, 'index.html')
|
||||
: join(rendererDir, resolvedPath)
|
||||
|
||||
const tryReadFile = async (path: string) => {
|
||||
const ext = extname(path)
|
||||
const mimeType = MIME_TYPES[ext] ?? 'application/octet-stream'
|
||||
const data = await readFile(path)
|
||||
return new Response(data, { headers: { 'Content-Type': mimeType } })
|
||||
}
|
||||
|
||||
try {
|
||||
return await tryReadFile(filePath)
|
||||
} catch {
|
||||
const indexPath = join(rendererDir, 'index.html')
|
||||
return tryReadFile(indexPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user