fix: 修正 ORPC handler 语义、加固 Electron 安全、优化构建与运行时配置
- todo.router: create 错误码 NOT_FOUND → INTERNAL_SERVER_ERROR,remove 增加存在性检查 - __root: devtools 仅在 DEV 环境渲染 - Electron: 添加 will-navigate 导航拦截、显式安全 webPreferences、deny-all 权限请求 - sidecar: 空 catch 块补充意图注释,新增 lastResolvedUrl getter - todo.contract: 硬编码 omit 改用 generatedFieldKeys - router: QueryClient 添加 staleTime/retry 默认值 - turbo: build 任务精细化 inputs 提升缓存命中率 - fields: id() 改为模块私有
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { app, BrowserWindow, dialog, shell } from 'electron'
|
import { app, BrowserWindow, dialog, session, shell } from 'electron'
|
||||||
import { createSidecarRuntime } from './sidecar'
|
import { createSidecarRuntime } from './sidecar'
|
||||||
|
|
||||||
const DEV_SERVER_URL = 'http://localhost:3000'
|
const DEV_SERVER_URL = 'http://localhost:3000'
|
||||||
@@ -61,6 +61,8 @@ const createWindow = async () => {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
mainWindow = windowRef
|
mainWindow = windowRef
|
||||||
@@ -78,6 +80,21 @@ const createWindow = async () => {
|
|||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
windowRef.webContents.on('will-navigate', (event, url) => {
|
||||||
|
const allowed = [DEV_SERVER_URL, sidecar.lastResolvedUrl].filter((v): v is string => v != null)
|
||||||
|
const isAllowed = allowed.some((origin) => url.startsWith(origin))
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (canOpenExternally(url)) {
|
||||||
|
void shell.openExternal(url)
|
||||||
|
} else if (!app.isPackaged) {
|
||||||
|
console.warn(`Blocked navigation to: ${url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
windowRef.on('closed', () => {
|
windowRef.on('closed', () => {
|
||||||
if (mainWindow === windowRef) {
|
if (mainWindow === windowRef) {
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
@@ -151,7 +168,13 @@ const handleWindowCreationError = (error: unknown, context: string) => {
|
|||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(() => ensureWindow())
|
.then(() => {
|
||||||
|
session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => {
|
||||||
|
callback(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return ensureWindow()
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
handleWindowCreationError(error, 'Failed to create window')
|
handleWindowCreationError(error, 'Failed to create window')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type SidecarRuntimeOptions = {
|
|||||||
type SidecarRuntime = {
|
type SidecarRuntime = {
|
||||||
resolveUrl: () => Promise<string>
|
resolveUrl: () => Promise<string>
|
||||||
stop: () => void
|
stop: () => void
|
||||||
|
lastResolvedUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
@@ -72,7 +73,9 @@ const isServerReady = async (url: string): Promise<boolean> => {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {
|
||||||
|
// Expected: probe request fails while server is still starting up
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -239,11 +242,15 @@ export const createSidecarRuntime = (options: SidecarRuntimeOptions): SidecarRun
|
|||||||
throw new Error('Dev server not responding. Run `bun dev` in apps/server first.')
|
throw new Error('Dev server not responding. Run `bun dev` in apps/server first.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.url = options.devServerUrl
|
||||||
return options.devServerUrl
|
return options.devServerUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resolveUrl,
|
resolveUrl,
|
||||||
stop,
|
stop,
|
||||||
|
get lastResolvedUrl() {
|
||||||
|
return state.url
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import type { RouterContext } from './routes/__root'
|
|||||||
import { routeTree } from './routeTree.gen'
|
import { routeTree } from './routeTree.gen'
|
||||||
|
|
||||||
export const getRouter = () => {
|
export const getRouter = () => {
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
|
{import.meta.env.DEV && (
|
||||||
<TanStackDevtools
|
<TanStackDevtools
|
||||||
config={{
|
config={{
|
||||||
position: 'bottom-right',
|
position: 'bottom-right',
|
||||||
@@ -61,6 +62,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { oc } from '@orpc/contract'
|
import { oc } from '@orpc/contract'
|
||||||
import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-orm/zod'
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-orm/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { generatedFieldKeys } from '@/server/db/fields'
|
||||||
import { todoTable } from '@/server/db/schema'
|
import { todoTable } from '@/server/db/schema'
|
||||||
|
|
||||||
const selectSchema = createSelectSchema(todoTable)
|
const selectSchema = createSelectSchema(todoTable)
|
||||||
|
|
||||||
const insertSchema = createInsertSchema(todoTable).omit({
|
const insertSchema = createInsertSchema(todoTable).omit(generatedFieldKeys)
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateSchema = createUpdateSchema(todoTable).omit({
|
const updateSchema = createUpdateSchema(todoTable).omit(generatedFieldKeys)
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const list = oc.input(z.void()).output(z.array(selectSchema))
|
export const list = oc.input(z.void()).output(z.array(selectSchema))
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const create = os.todo.create.use(db).handler(async ({ context, input })
|
|||||||
const [newTodo] = await context.db.insert(todoTable).values(input).returning()
|
const [newTodo] = await context.db.insert(todoTable).values(input).returning()
|
||||||
|
|
||||||
if (!newTodo) {
|
if (!newTodo) {
|
||||||
throw new ORPCError('NOT_FOUND')
|
throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create todo' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTodo
|
return newTodo
|
||||||
@@ -32,5 +32,9 @@ export const update = os.todo.update.use(db).handler(async ({ context, input })
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const remove = os.todo.remove.use(db).handler(async ({ context, input }) => {
|
export const remove = os.todo.remove.use(db).handler(async ({ context, input }) => {
|
||||||
await context.db.delete(todoTable).where(eq(todoTable.id, input.id))
|
const [deleted] = await context.db.delete(todoTable).where(eq(todoTable.id, input.id)).returning({ id: todoTable.id })
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
throw new ORPCError('NOT_FOUND')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { v7 as uuidv7 } from 'uuid'
|
|||||||
|
|
||||||
// id
|
// id
|
||||||
|
|
||||||
export const id = (name: string) => uuid(name)
|
const id = (name: string) => uuid(name)
|
||||||
export const pk = (name: string, strategy?: 'native' | 'extension') => {
|
export const pk = (name: string, strategy?: 'native' | 'extension') => {
|
||||||
switch (strategy) {
|
switch (strategy) {
|
||||||
// PG 18+
|
// PG 18+
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"env": ["NODE_ENV", "VITE_*"],
|
"env": ["NODE_ENV", "VITE_*"],
|
||||||
|
"inputs": ["src/**", "public/**", "package.json", "tsconfig.json", "vite.config.ts"],
|
||||||
"outputs": [".output/**"]
|
"outputs": [".output/**"]
|
||||||
},
|
},
|
||||||
"compile": {
|
"compile": {
|
||||||
|
|||||||
Reference in New Issue
Block a user