@allbluecn/admin-app 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/package.json +19 -17
  2. package/src/components/admin/admin-user-menu.tsx +4 -4
  3. package/src/components/admin/developer-log.tsx +16 -32
  4. package/src/components/ai-memory/memory-facts-panel.tsx +30 -70
  5. package/src/components/ai-prompts/preview-panel.tsx +1 -1
  6. package/src/components/ai-prompts/prompt-editor.tsx +8 -9
  7. package/src/components/ai-prompts/prompt-table.tsx +6 -6
  8. package/src/components/ai-prompts/section-editor.tsx +3 -3
  9. package/src/components/ai-prompts/variable-panel.tsx +2 -2
  10. package/src/components/ai-prompts/version-history.tsx +21 -41
  11. package/src/components/ai-provider/model-list-editor.tsx +2 -3
  12. package/src/components/ai-provider/provider-form.tsx +7 -7
  13. package/src/components/ai-provider/provider-table.tsx +17 -40
  14. package/src/components/auth/admin-login-form.tsx +7 -7
  15. package/src/components/auth/icon-input.tsx +2 -2
  16. package/src/components/auth/password-input.tsx +2 -2
  17. package/src/components/component/component-form-dialog.tsx +10 -18
  18. package/src/components/component/component-import-dialog.tsx +6 -6
  19. package/src/components/component/component-preview.tsx +311 -0
  20. package/src/components/component/component-table.tsx +3 -3
  21. package/src/components/dependency/changelog-dialog.tsx +2 -2
  22. package/src/components/dependency/columns.tsx +3 -3
  23. package/src/components/dependency/dependency-table.tsx +7 -7
  24. package/src/components/dependency/edit-dialog.tsx +8 -8
  25. package/src/components/invitations/invitations-page.tsx +26 -49
  26. package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
  27. package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
  28. package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
  29. package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -2
  30. package/src/components/layout/sidebar-02/index.tsx +1 -1
  31. package/src/components/layout/sidebar-02/nav-main.tsx +2 -3
  32. package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
  33. package/src/components/market/columns.tsx +4 -4
  34. package/src/components/market/market-app-form.tsx +6 -7
  35. package/src/components/market/market-app-list.tsx +14 -29
  36. package/src/components/monitoring/error-list.tsx +2 -2
  37. package/src/components/monitoring/metrics-stats.tsx +1 -1
  38. package/src/components/monitoring/request-log-table.tsx +2 -2
  39. package/src/components/oauth-providers/provider-page.tsx +20 -38
  40. package/src/components/permissions/assign-dialog.tsx +135 -0
  41. package/src/components/permissions/create-role-dialog.tsx +135 -0
  42. package/src/components/permissions/types.ts +49 -0
  43. package/src/components/user/columns.tsx +6 -6
  44. package/src/components/user/settings-activity-log.tsx +7 -7
  45. package/src/components/user/settings-profile/avatar-uploader.tsx +118 -0
  46. package/src/components/user/settings-profile/social-links.tsx +63 -0
  47. package/src/components/user/settings-profile.tsx +26 -125
  48. package/src/components/user/user-create-dialog.tsx +6 -6
  49. package/src/components/user/user-detail/error-boundary.tsx +19 -0
  50. package/src/components/user/user-detail/user-detail-dialogs.tsx +203 -0
  51. package/src/components/user/user-filters.tsx +4 -4
  52. package/src/lib/auth/page-gate.ts +28 -0
  53. package/src/lib/auth.config.ts +25 -9
  54. package/src/lib/i18n/detect.ts +1 -1
  55. package/src/lib/loader-serial.ts +14 -0
  56. package/src/providers/index.tsx +1 -1
  57. package/src/routeTree.gen.ts +67 -78
  58. package/src/routes/__root.tsx +4 -4
  59. package/src/routes/_nav/audit/route.lazy.tsx +1 -2
  60. package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
  61. package/src/routes/_nav/components/route.lazy.tsx +14 -34
  62. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
  63. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
  64. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
  65. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
  66. package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
  67. package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
  68. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
  69. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
  70. package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
  71. package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
  72. package/src/routes/_nav/monitoring/health/route.tsx +14 -0
  73. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
  74. package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
  75. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
  76. package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
  77. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
  78. package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
  79. package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
  80. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
  81. package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
  82. package/src/routes/_nav/permissions/route.tsx +1 -1
  83. package/src/routes/_nav/route.tsx +5 -13
  84. package/src/routes/_nav/sso/route.lazy.tsx +2 -3
  85. package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
  86. package/src/routes/_nav/users/-components/users-table.tsx +1 -1
  87. package/src/routes/_nav/users/route.lazy.tsx +1 -1
  88. package/src/routes/login/route.lazy.tsx +1 -1
  89. package/src/routes/setup/route.lazy.tsx +2 -2
  90. package/src/server/__tests__/admin-billing.test.ts +191 -16
  91. package/src/server/__tests__/compliance.test.ts +44 -7
  92. package/src/server/__tests__/permissions.test.ts +10 -5
  93. package/src/server/__tests__/prompt-defaults.test.ts +3 -3
  94. package/src/server/admin-ai-alerts.ts +2 -1
  95. package/src/server/admin-ai-usage.ts +2 -1
  96. package/src/server/ai-memory/store.ts +6 -2
  97. package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
  98. package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
  99. package/src/server/knowledge-public/github-sync.ts +2 -1
  100. package/src/server/license-issue.ts +2 -1
  101. package/src/server/middlewares/auth-rate-limit.ts +117 -0
  102. package/src/server/observability/health-check.ts +4 -1
  103. package/src/server/observability/metrics.ts +17 -15
  104. package/src/server/observability/request-middleware.ts +2 -1
  105. package/src/server/observability/timing.ts +14 -16
  106. package/src/server/serverFns/admin-billing.ts +107 -12
  107. package/src/server/serverFns/ai-memory/list-users.ts +2 -1
  108. package/src/server/serverFns/ai-prompts/activate.ts +2 -1
  109. package/src/server/serverFns/ai-prompts/get.ts +3 -2
  110. package/src/server/serverFns/ai-prompts/list.ts +3 -2
  111. package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
  112. package/src/server/serverFns/ai-prompts/save.ts +5 -3
  113. package/src/server/serverFns/ai-provider/create.ts +4 -1
  114. package/src/server/serverFns/ai-provider/delete.ts +4 -1
  115. package/src/server/serverFns/ai-provider/get.ts +4 -1
  116. package/src/server/serverFns/ai-provider/list.ts +28 -1
  117. package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
  118. package/src/server/serverFns/ai-provider/update.ts +4 -1
  119. package/src/server/serverFns/anyhub/market.ts +8 -1
  120. package/src/server/serverFns/audit.ts +14 -1
  121. package/src/server/serverFns/auth/setup.ts +3 -1
  122. package/src/server/serverFns/compliance.ts +32 -3
  123. package/src/server/serverFns/components/import.ts +69 -14
  124. package/src/server/serverFns/components/list.ts +4 -1
  125. package/src/server/serverFns/components/mutations.ts +9 -1
  126. package/src/server/serverFns/dependency/list.ts +28 -5
  127. package/src/server/serverFns/dependency/sync.ts +7 -2
  128. package/src/server/serverFns/dependency/update.ts +4 -1
  129. package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
  130. package/src/server/serverFns/developer-logs.ts +5 -1
  131. package/src/server/serverFns/invitations.ts +7 -1
  132. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
  133. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
  134. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
  135. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
  136. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
  137. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
  138. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  139. package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
  140. package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
  141. package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
  142. package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
  143. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  144. package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
  145. package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
  146. package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
  147. package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
  148. package/src/server/serverFns/licenses.ts +8 -2
  149. package/src/server/serverFns/monitoring/get-errors.ts +2 -1
  150. package/src/server/serverFns/monitoring/get-health.ts +2 -1
  151. package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
  152. package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
  153. package/src/server/serverFns/monitoring/report-error.ts +2 -1
  154. package/src/server/serverFns/monitoring/reset.ts +2 -1
  155. package/src/server/serverFns/oauth-providers.ts +4 -1
  156. package/src/server/serverFns/permissions.ts +18 -11
  157. package/src/server/serverFns/sentry/config.ts +11 -8
  158. package/src/server/serverFns/sso-config.ts +17 -2
  159. package/src/server/serverFns/user/create.ts +6 -1
  160. package/src/server/serverFns/user/delete.ts +6 -1
  161. package/src/server/serverFns/user/detail.ts +5 -2
  162. package/src/server/serverFns/user/list.ts +19 -4
  163. package/src/server/serverFns/user/reset-password.ts +10 -2
  164. package/src/server/serverFns/user/update-role.ts +10 -2
  165. package/src/server/serverFns/user/update-status.ts +10 -2
  166. package/src/styles/globals.css +4 -0
  167. package/src/components/data-table/data-table-pagination.tsx +0 -112
  168. package/src/components/data-table/data-table.tsx +0 -218
  169. package/src/components/data-table/table-detail-layout.tsx +0 -98
  170. package/src/components/ui/alert-dialog.tsx +0 -187
  171. package/src/components/ui/alert.tsx +0 -75
  172. package/src/components/ui/avatar.tsx +0 -106
  173. package/src/components/ui/badge.tsx +0 -51
  174. package/src/components/ui/button.tsx +0 -57
  175. package/src/components/ui/card.tsx +0 -102
  176. package/src/components/ui/checkbox.tsx +0 -28
  177. package/src/components/ui/collapsible.tsx +0 -19
  178. package/src/components/ui/dialog.tsx +0 -155
  179. package/src/components/ui/dropdown-menu.tsx +0 -267
  180. package/src/components/ui/field.tsx +0 -236
  181. package/src/components/ui/input-group.tsx +0 -158
  182. package/src/components/ui/input.tsx +0 -19
  183. package/src/components/ui/label.tsx +0 -17
  184. package/src/components/ui/pagination.tsx +0 -130
  185. package/src/components/ui/select.tsx +0 -234
  186. package/src/components/ui/separator.tsx +0 -22
  187. package/src/components/ui/sheet.tsx +0 -135
  188. package/src/components/ui/sidebar.tsx +0 -721
  189. package/src/components/ui/skeleton.tsx +0 -13
  190. package/src/components/ui/switch.tsx +0 -29
  191. package/src/components/ui/table.tsx +0 -115
  192. package/src/components/ui/textarea.tsx +0 -17
  193. package/src/components/ui/toast.tsx +0 -282
  194. package/src/components/ui/tooltip.tsx +0 -63
  195. package/src/hooks/use-mobile.ts +0 -19
  196. package/src/lib/avatar.ts +0 -41
  197. /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
  198. /package/src/routes/{api.sentry-test.ts → api/sentry-test.ts} +0 -0
@@ -17,21 +17,52 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { execFileSync } from 'node:child_process'
22
22
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs'
23
23
  import { resolve, join } from 'node:path'
24
-
25
- const PACKAGE_NAME_RE = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i
24
+ import { requireAdmin } from '@/server/auth-helpers'
25
+
26
+ // ── 供应链风险决策(SEC-01 联动)──────────────────────────────────────────
27
+ // `pnpm add <包名>` 会执行该包的 postinstall 等生命周期脚本,等同在服务器上
28
+ // 任意代码执行;`pnpm dlx shadcn add <path>` 也会按 registry 路径拉取并写入
29
+ // 任意来源的组件文件。除 requireAdmin 鉴权外,追加以下防线:
30
+ // ① 包名白名单格式:仅小写字母/数字开头的扁平包名(允许 . _ -),拒绝
31
+ // @scope、`/`(GitHub shorthand 与路径分隔符,防 a/../../evil 绕过
32
+ // registry 固定)、URL、绝对路径等一切非常规形态;
33
+ // ② registry 显式固定为 npm 官方源,防止 .npmrc / 环境变量被劫持改指向
34
+ // 恶意镜像(typosquatting 面收窄到官方 registry 的审计能力内);
35
+ // ③ --ignore-scripts:跳过被安装包的全部生命周期脚本,组件类纯 JS 包
36
+ // 无需 install 脚本,个别依赖构建脚本的包会装出不可用组件——宁可
37
+ // 导入失败人工介入,不在服务端静默执行第三方脚本;
38
+ // ④ shadcn 路径(dlx 分支)校验为合法 registry item 形态(组件名或
39
+ // @scope/name),拒绝任意 URL / 绝对路径;@/ 本地路径同样做段字符
40
+ // 白名单(拒引号/`${`/空白等模板注入字符)且 resolve 后强制约束在
41
+ // apps/web/src 之内,堵死路径穿越式任意文件读;
42
+ // ⑤ 所有插值进生成的 .tsx wrapper 的字符串(displayName、exportName、
43
+ // importSource、componentKey)一律先过字符白名单/标识符校验——
44
+ // wrapper 是会被 Vite 编译执行的代码,插值即模板注入面。
45
+ // ─────────────────────────────────────────────────────────────────────────
46
+ const NPM_PACKAGE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/
47
+ const SHADCN_REGISTRY_PATH_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/
48
+ // @/ 本地路径:段字符白名单 [a-z0-9._-],段必须以字母/数字开头(. 与 ..
49
+ // 段天然被拒),拒引号、反斜杠、空白、`${}` 等一切模板注入字符
50
+ const LOCAL_IMPORT_PATH_RE = /^@\/[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/
51
+ // displayName 会插值进 wrapper 的 `name: "${displayName}"`:仅允许中文、
52
+ // 字母、数字、空格与 . _ -,拒引号/反引号/反斜杠/`${}`/尖括号等
53
+ const DISPLAY_NAME_RE = /^[\p{Script=Han}a-zA-Z0-9][\p{Script=Han}a-zA-Z0-9 ._-]*$/u
54
+ // exportName 会插值进 `import { x as Comp } from ...`:必须为合法 JS 标识符
55
+ const EXPORT_NAME_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/
56
+ const PINNED_REGISTRY = 'https://registry.npmjs.org/'
26
57
  const COMPONENT_KEY_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
27
58
 
28
59
  const importInput = z.object({
29
60
  source: z.enum(['npm', 'local']).default('npm'),
30
- packageName: z.string().regex(PACKAGE_NAME_RE, '无效的 npm 包名').optional(),
61
+ packageName: z.string().regex(NPM_PACKAGE_NAME_RE, '无效的 npm 包名(仅允许小写字母、数字与 . _ -,拒绝 @scope 与斜杠)').optional(),
31
62
  importPath: z.string().min(1, '不能为空').optional(),
32
- exportName: z.string().default('default'),
63
+ exportName: z.string().regex(EXPORT_NAME_RE, '导出名必须为合法标识符').default('default'),
33
64
  componentKey: z.string().regex(COMPONENT_KEY_RE, '标识只能含小写字母、数字、连字符'),
34
- displayName: z.string().min(1, '请输入显示名称'),
65
+ displayName: z.string().regex(DISPLAY_NAME_RE, '显示名称只能含中文、字母、数字、空格与 . _ -'),
35
66
  category: z.enum(['基础组件', '内容区块', '页面模版']),
36
67
  type: z.enum(['component', 'block', 'template']).default('component'),
37
68
  defaultWidth: z.number().int().positive().default(200),
@@ -64,6 +95,8 @@ function detectExportName(filePath: string, fallback: string): string {
64
95
  export const importComponentFn = createServerFn({ method: 'POST' })
65
96
  .validator(importInput)
66
97
  .handler(async ({ data }) => {
98
+ const prisma = await getPrisma()
99
+ await requireAdmin()
67
100
  const {
68
101
  source,
69
102
  packageName,
@@ -87,13 +120,20 @@ export const importComponentFn = createServerFn({ method: 'POST' })
87
120
 
88
121
  if (source === 'npm') {
89
122
  if (!packageName) throw new Error('npm 包名不能为空')
123
+ if (!NPM_PACKAGE_NAME_RE.test(packageName)) {
124
+ throw new Error(`拒绝安装非常规包名 "${packageName}":仅允许小写字母/数字开头的非 scope 包(供应链防护)`)
125
+ }
90
126
  const prototypeDir = join(monorepoRoot, 'packages/prototype')
91
127
  try {
92
- execFileSync('pnpm', ['add', packageName], {
93
- cwd: prototypeDir,
94
- stdio: 'pipe',
95
- timeout: 60000,
96
- })
128
+ execFileSync(
129
+ 'pnpm',
130
+ ['add', packageName, `--registry=${PINNED_REGISTRY}`, '--ignore-scripts'],
131
+ {
132
+ cwd: prototypeDir,
133
+ stdio: 'pipe',
134
+ timeout: 60000,
135
+ },
136
+ )
97
137
  } catch {
98
138
  throw new Error(`安装包 "${packageName}" 失败,请检查包名或网络`)
99
139
  }
@@ -107,11 +147,25 @@ export const importComponentFn = createServerFn({ method: 'POST' })
107
147
  let componentFilePath: string | null = null
108
148
 
109
149
  if (importPath.startsWith('@/')) {
150
+ // 段字符白名单:拒引号/`${`/空白等模板注入字符与 . .. 段(与 resolve
151
+ // 前缀检查构成双防线——前者护 wrapper 代码生成,后者护文件读取)
152
+ if (!LOCAL_IMPORT_PATH_RE.test(importPath)) {
153
+ throw new Error(`拒绝非法本地路径 "${importPath}":仅允许 @/ 开头的小写路径段(供应链防护)`)
154
+ }
155
+ const resolvedPath = resolve(webSrc, `${importPath.slice(2)}.tsx`)
156
+ // 路径穿越防护:@/ 别名只允许解析到 apps/web/src 内部的 .tsx
157
+ if (!resolvedPath.startsWith(webSrc + '/')) {
158
+ throw new Error('非法路径:importPath 越界')
159
+ }
110
160
  importSource = importPath
111
- const relPath = importPath.slice(2)
112
- const tryPath = join(webSrc, `${relPath}.tsx`)
113
- if (existsSync(tryPath)) componentFilePath = tryPath
161
+ if (existsSync(resolvedPath)) componentFilePath = resolvedPath
114
162
  } else {
163
+ // registry 路径(如 @magicui/safari 或 button):拒绝 URL / 绝对路径 / ..
164
+ // shadcn CLI 无 --registry 旗标,registry 来源由项目 components.json 的
165
+ // 受信 registries 配置决定;此处以路径格式校验挡住任意 URL / 路径注入。
166
+ if (!SHADCN_REGISTRY_PATH_RE.test(importPath)) {
167
+ throw new Error(`拒绝非 registry 路径 "${importPath}":仅允许组件名或 @scope/name(供应链防护)`)
168
+ }
115
169
  const compName = importPath.split('/').pop() ?? importPath
116
170
  try {
117
171
  execFileSync('pnpm', ['dlx', 'shadcn@latest', 'add', importPath, '--yes'], {
@@ -158,6 +212,7 @@ import React from "react";
158
212
  ${importStmt}
159
213
 
160
214
  export function Imported${pascalName}({ props, style }: { props?: Record<string, any>; style?: Record<string, any> }) {
215
+ const prisma = await getPrisma()
161
216
  return <Comp {...props} style={style} />;
162
217
  }
163
218
 
@@ -17,7 +17,8 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { requireAdmin } from '@/server/auth-helpers'
21
22
 
22
23
  const listInput = z.object({
23
24
  page: z.number().default(1),
@@ -29,6 +30,8 @@ const listInput = z.object({
29
30
  export const listComponentsFn = createServerFn({ method: 'GET' })
30
31
  .validator(listInput)
31
32
  .handler(async ({ data }) => {
33
+ const prisma = await getPrisma()
34
+ await requireAdmin()
32
35
  const { page, pageSize, search, category } = data
33
36
  const where: any = { isDeleted: false }
34
37
  if (search) {
@@ -17,7 +17,9 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma, Prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import type { Prisma } from '@allbluecn/database'
22
+ import { requireAdmin } from '@/server/auth-helpers'
21
23
 
22
24
  const createInput = z.object({
23
25
  name: z.string().min(1),
@@ -29,6 +31,8 @@ const createInput = z.object({
29
31
  export const createComponentFn = createServerFn({ method: 'POST' })
30
32
  .validator(createInput)
31
33
  .handler(async ({ data }) => {
34
+ const prisma = await getPrisma()
35
+ await requireAdmin()
32
36
  return prisma.component.create({
33
37
  data: {
34
38
  name: data.name,
@@ -61,6 +65,8 @@ const updateInput = z.object({
61
65
  export const updateComponentFn = createServerFn({ method: 'POST' })
62
66
  .validator(updateInput)
63
67
  .handler(async ({ data }) => {
68
+ const prisma = await getPrisma()
69
+ await requireAdmin()
64
70
  const { id, defaultProps, defaultStyle, ...rest } = data
65
71
  return prisma.component.update({
66
72
  where: { id },
@@ -77,6 +83,8 @@ const deleteInput = z.object({ id: z.string() })
77
83
  export const deleteComponentFn = createServerFn({ method: 'POST' })
78
84
  .validator(deleteInput)
79
85
  .handler(async ({ data }) => {
86
+ const prisma = await getPrisma()
87
+ await requireAdmin()
80
88
  return prisma.component.update({
81
89
  where: { id: data.id },
82
90
  data: { isDeleted: true },
@@ -18,8 +18,9 @@
18
18
  // apps/admin/src/server/serverFns/dependency/list.ts
19
19
  import { createServerFn } from "@tanstack/react-start"
20
20
  import { z } from "zod"
21
- import { prisma } from "@allbluecn/database"
21
+ import { getPrisma } from "@allbluecn/database/get-prisma"
22
22
  import type { ThirdPartyDependencyRow } from "@allbluecn/kernel"
23
+ import { requireAdmin } from "@/server/auth-helpers"
23
24
  import {
24
25
  aggregateUsages,
25
26
  compareVersions,
@@ -27,12 +28,13 @@ import {
27
28
  npmUrlOf,
28
29
  pickCurrentVersion,
29
30
  readMonorepoPackages,
30
- } from "./helpers"
31
+ } from "@/server/dependency/helpers"
31
32
 
32
33
  const listInput = z.object({
33
34
  /** 仅返回关注列表 */
34
35
  watchedOnly: z.boolean().optional(),
35
- category: z.string().optional(),
36
+ // 与 DB enum DependencyCategory(kernel DependencyCategory 联合类型)对齐,同 update.ts
37
+ category: z.enum(["framework", "ui", "editor", "ai", "state", "auth", "tool", "other"]).optional(),
36
38
  search: z.string().optional(),
37
39
  })
38
40
 
@@ -46,6 +48,8 @@ const listInput = z.object({
46
48
  export const listDependenciesFn = createServerFn({ method: "GET" })
47
49
  .validator(listInput)
48
50
  .handler(async ({ data }) => {
51
+ const prisma = await getPrisma()
52
+ await requireAdmin()
49
53
  await ensureSeedData()
50
54
 
51
55
  // 一次读取所有 package.json,供聚合复用
@@ -60,10 +64,29 @@ export const listDependenciesFn = createServerFn({ method: "GET" })
60
64
  }
61
65
 
62
66
  // 读 DB 所有记录
63
- const where: { isWatched?: boolean; category?: string } = {}
67
+ const where: { isWatched?: boolean; category?: "framework" | "ui" | "editor" | "ai" | "state" | "auth" | "tool" | "other" } = {}
64
68
  if (data.watchedOnly) where.isWatched = true
65
69
  if (data.category) where.category = data.category
66
- const dbRows = await prisma.thirdPartyDependency.findMany({ where })
70
+ // DB-05:白名单 select(createdAt 不进聚合结果,整行排除)
71
+ const dbRows = await prisma.thirdPartyDependency.findMany({
72
+ where,
73
+ select: {
74
+ id: true,
75
+ name: true,
76
+ displayName: true,
77
+ description: true,
78
+ homepage: true,
79
+ category: true,
80
+ isWatched: true,
81
+ priority: true,
82
+ latestVersion: true,
83
+ latestChangelog: true,
84
+ lastSyncAt: true,
85
+ notes: true,
86
+ sortOrder: true,
87
+ updatedAt: true,
88
+ },
89
+ })
67
90
  const dbMap = new Map(dbRows.map((r) => [r.name, r]))
68
91
 
69
92
  // 构造结果集:DB 中所有记录 + package.json 中存在但 DB 未跟踪的包
@@ -18,9 +18,10 @@
18
18
  // apps/admin/src/server/serverFns/dependency/sync.ts
19
19
  import { createServerFn } from "@tanstack/react-start"
20
20
  import { z } from "zod"
21
- import { prisma } from "@allbluecn/database"
21
+ import { getPrisma } from "@allbluecn/database/get-prisma"
22
22
  import type { SyncDependencyResult } from "@allbluecn/kernel"
23
- import { queryNpmRegistry } from "./helpers"
23
+ import { requireAdmin } from "@/server/auth-helpers"
24
+ import { queryNpmRegistry } from "@/server/dependency/helpers"
24
25
 
25
26
  const syncInput = z.object({
26
27
  /** npm 包名;不存在于 DB 时会 upsert 创建 */
@@ -34,6 +35,8 @@ const syncInput = z.object({
34
35
  export const syncDependencyFn = createServerFn({ method: "POST" })
35
36
  .validator(syncInput)
36
37
  .handler(async ({ data }): Promise<SyncDependencyResult> => {
38
+ const prisma = await getPrisma()
39
+ await requireAdmin()
37
40
  try {
38
41
  const { latestVersion, homepage, changelog } = await queryNpmRegistry(data.name)
39
42
  await prisma.thirdPartyDependency.upsert({
@@ -85,6 +88,8 @@ const syncAllInput = z.object({
85
88
  export const syncAllDependenciesFn = createServerFn({ method: "POST" })
86
89
  .validator(syncAllInput)
87
90
  .handler(async ({ data }): Promise<SyncDependencyResult[]> => {
91
+ const prisma = await getPrisma()
92
+ await requireAdmin()
88
93
  const results: SyncDependencyResult[] = []
89
94
  // 顺序执行(npm registry 建议不并发轰炸)
90
95
  for (const name of data.names) {
@@ -18,8 +18,9 @@
18
18
  // apps/admin/src/server/serverFns/dependency/update.ts
19
19
  import { createServerFn } from "@tanstack/react-start"
20
20
  import { z } from "zod"
21
- import { prisma } from "@allbluecn/database"
21
+ import { getPrisma } from "@allbluecn/database/get-prisma"
22
22
  import type { UpdateDependencyInput } from "@allbluecn/kernel"
23
+ import { requireAdmin } from "@/server/auth-helpers"
23
24
 
24
25
  const updateInput = z.object({
25
26
  id: z.string().min(1),
@@ -37,6 +38,8 @@ const updateInput = z.object({
37
38
  export const updateDependencyFn = createServerFn({ method: "POST" })
38
39
  .validator(updateInput)
39
40
  .handler(async ({ data }) => {
41
+ const prisma = await getPrisma()
42
+ await requireAdmin()
40
43
  const { id, ...patch } = data
41
44
  // 过滤 undefined 字段,避免覆盖为 null
42
45
  const cleanPatch: Record<string, unknown> = {}
@@ -1,6 +1,6 @@
1
1
  import { createServerFn } from '@tanstack/react-start'
2
2
  import { z } from 'zod'
3
- import { prisma } from '@allbluecn/database'
3
+ import { getPrisma } from '@allbluecn/database/get-prisma'
4
4
  import { execFileSync } from 'node:child_process'
5
5
  import path from 'node:path'
6
6
  import { requireAdmin } from '@/server/auth-helpers'
@@ -86,6 +86,7 @@ export const autoGenerateLogsFn = createServerFn({ method: 'POST' })
86
86
  daysBack: z.number().default(60),
87
87
  }))
88
88
  .handler(async ({ data }) => {
89
+ const prisma = await getPrisma()
89
90
  await requireAdmin()
90
91
 
91
92
  const repoRoot = path.resolve(process.cwd(), '../..')
@@ -97,8 +98,20 @@ export const autoGenerateLogsFn = createServerFn({ method: 'POST' })
97
98
  )
98
99
 
99
100
  const commitBlocks = stdout.split('\n\n').filter((block) => block.trim())
100
- let created = 0
101
-
101
+ // DB-01:查重从逐 commit findFirst(N 次查询)改为一次 findMany in 预取 sha 集合 + 批内存重去重;
102
+ // 写入用 createManyAndReturn 以精确统计实际插入行数(等价原 created 计数)。
103
+ // DB-07 核实(任务 3.4):写入已是单步批量 createManyAndReturn(Prisma 单语句自带
104
+ // 原子性),无多步半批残留风险,按任务 3.3 先例不再包 $transaction。
105
+ // 查询次数 before: 2N → after: 2
106
+ interface PendingLog {
107
+ date: string
108
+ year: string
109
+ title: string
110
+ desc: string
111
+ tags: string[]
112
+ commitSha: string
113
+ }
114
+ const pending = new Map<string, PendingLog>()
102
115
  for (const block of commitBlocks) {
103
116
  const lines = block.trim().split('\n')
104
117
  if (lines.length < 2) continue
@@ -117,24 +130,34 @@ export const autoGenerateLogsFn = createServerFn({ method: 'POST' })
117
130
 
118
131
  const { date, year } = formatCommitDate(dateStr)
119
132
 
120
- const existing = await prisma.developerLog.findFirst({
121
- where: { commitSha: sha, isDeleted: false },
133
+ // 批内重复 sha:首个入 Map,后续跳过(与原「已存在则 continue」语义一致)
134
+ if (pending.has(sha)) continue
135
+ pending.set(sha, {
136
+ date,
137
+ year,
138
+ title: parsed.title,
139
+ desc: generateMarkdownDetail(parsed.title, parsed.body, sha, dateStr),
140
+ tags: parsed.tags,
141
+ commitSha: sha,
122
142
  })
123
- if (existing) continue
124
-
125
- const descMarkdown = generateMarkdownDetail(parsed.title, parsed.body, sha, dateStr)
126
-
127
- await prisma.developerLog.create({
128
- data: {
129
- date,
130
- year,
131
- title: parsed.title,
132
- desc: descMarkdown,
133
- tags: parsed.tags,
134
- commitSha: sha,
135
- },
136
- })
137
- created++
143
+ }
144
+
145
+ let created = 0
146
+ if (pending.size > 0) {
147
+ const shas = [...pending.keys()]
148
+ const existingShas = new Set(
149
+ (
150
+ await prisma.developerLog.findMany({
151
+ where: { commitSha: { in: shas }, isDeleted: false },
152
+ select: { commitSha: true },
153
+ })
154
+ ).map((row) => row.commitSha),
155
+ )
156
+ const toCreate = [...pending.values()].filter((row) => !existingShas.has(row.commitSha))
157
+ if (toCreate.length > 0) {
158
+ const inserted = await prisma.developerLog.createManyAndReturn({ data: toCreate })
159
+ created = inserted.length
160
+ }
138
161
  }
139
162
 
140
163
  return { success: true, count: created }
@@ -145,6 +168,7 @@ export const autoGenerateLogsFn = createServerFn({ method: 'POST' })
145
168
 
146
169
  export const backfillLogDetailsFn = createServerFn({ method: 'POST' })
147
170
  .handler(async () => {
171
+ const prisma = await getPrisma()
148
172
  await requireAdmin()
149
173
 
150
174
  const entries = await prisma.developerLog.findMany({
@@ -157,19 +181,26 @@ export const backfillLogDetailsFn = createServerFn({ method: 'POST' })
157
181
  },
158
182
  })
159
183
 
160
- let updated = 0
161
-
162
- for (const entry of entries) {
163
- const hasMarkdown = entry.desc.includes('## ') || entry.desc.includes('```') || entry.desc.includes('---')
164
- if (hasMarkdown) continue
184
+ // DB-01:待回填行(无 markdown)先内存过滤,更新并行提交。每行 desc 不同,
185
+ // 无法合并为单条 updateMany;原逐条串行 await 改 Promise.all 消除串行放大。
186
+ // 查询次数 before: N(串行) → after: N(并行)
187
+ const stale = entries.filter(
188
+ (entry) => !(entry.desc.includes('## ') || entry.desc.includes('```') || entry.desc.includes('---')),
189
+ )
165
190
 
166
- const descMarkdown = generateMarkdownDetailFromLog(entry.title, entry.desc, entry.commitSha || '')
167
-
168
- await prisma.developerLog.update({
169
- where: { id: entry.id },
170
- data: { desc: descMarkdown },
171
- })
172
- updated++
191
+ let updated = 0
192
+ if (stale.length > 0) {
193
+ const results = await Promise.all(
194
+ stale.map(async (entry) => {
195
+ const descMarkdown = generateMarkdownDetailFromLog(entry.title, entry.desc, entry.commitSha || '')
196
+ await prisma.developerLog.update({
197
+ where: { id: entry.id },
198
+ data: { desc: descMarkdown },
199
+ })
200
+ return true
201
+ }),
202
+ )
203
+ updated = results.length
173
204
  }
174
205
 
175
206
  return { success: true, count: updated }
@@ -1,6 +1,6 @@
1
1
  import { createServerFn } from '@tanstack/react-start'
2
2
  import { z } from 'zod'
3
- import { prisma } from '@allbluecn/database'
3
+ import { getPrisma } from '@allbluecn/database/get-prisma'
4
4
  import type { DeveloperLogEntry } from '@allbluecn/kernel'
5
5
  import { requireAdmin } from '@/server/auth-helpers'
6
6
 
@@ -24,6 +24,7 @@ export const listDeveloperLogsFn = createServerFn({ method: 'GET' })
24
24
  tag: z.string().optional(),
25
25
  }))
26
26
  .handler(async ({ data }) => {
27
+ const prisma = await getPrisma()
27
28
  await requireAdmin()
28
29
  const where: Record<string, unknown> = { isDeleted: false }
29
30
  if (data.search) {
@@ -67,6 +68,7 @@ export const createDeveloperLogFn = createServerFn({ method: 'POST' })
67
68
  commitSha: z.string().optional(),
68
69
  }))
69
70
  .handler(async ({ data }) => {
71
+ const prisma = await getPrisma()
70
72
  await requireAdmin()
71
73
  const log = await prisma.developerLog.create({
72
74
  data: {
@@ -98,6 +100,7 @@ export const updateDeveloperLogFn = createServerFn({ method: 'POST' })
98
100
  commitSha: z.string().nullable().optional(),
99
101
  }))
100
102
  .handler(async ({ data }) => {
103
+ const prisma = await getPrisma()
101
104
  await requireAdmin()
102
105
  const updateData: Record<string, unknown> = {}
103
106
  if (data.date !== undefined) updateData.date = data.date
@@ -123,6 +126,7 @@ export const updateDeveloperLogFn = createServerFn({ method: 'POST' })
123
126
  export const deleteDeveloperLogFn = createServerFn({ method: 'POST' })
124
127
  .validator(z.object({ id: z.string() }))
125
128
  .handler(async ({ data }) => {
129
+ const prisma = await getPrisma()
126
130
  await requireAdmin()
127
131
  await prisma.developerLog.update({
128
132
  where: { id: data.id },
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
22
 
23
23
  function randomInvitationCode(): string {
@@ -35,6 +35,7 @@ const pageSchema = z.object({
35
35
  export const listInvitationCodesFn = createServerFn({ method: 'GET' })
36
36
  .validator(pageSchema)
37
37
  .handler(async ({ data }) => {
38
+ const prisma = await getPrisma()
38
39
  await requireAdmin()
39
40
  const skip = (data.page - 1) * data.pageSize
40
41
  const [codes, total] = await Promise.all([
@@ -94,6 +95,7 @@ const createCodeSchema = z.object({
94
95
  export const createInvitationCodeFn = createServerFn({ method: 'POST' })
95
96
  .validator(createCodeSchema)
96
97
  .handler(async ({ data }) => {
98
+ const prisma = await getPrisma()
97
99
  const session = await requireAdmin()
98
100
  const code = data.code ? data.code.trim().toUpperCase() : randomInvitationCode()
99
101
  const row = await prisma.invitationCode.create({
@@ -112,6 +114,7 @@ const idSchema = z.object({ id: z.string().min(1) })
112
114
  export const toggleInvitationCodeFn = createServerFn({ method: 'POST' })
113
115
  .validator(idSchema)
114
116
  .handler(async ({ data }) => {
117
+ const prisma = await getPrisma()
115
118
  await requireAdmin()
116
119
  const row = await prisma.invitationCode.findUnique({ where: { id: data.id } })
117
120
  if (!row) throw new Error('邀请码不存在')
@@ -126,6 +129,7 @@ export const toggleInvitationCodeFn = createServerFn({ method: 'POST' })
126
129
  export const deleteInvitationCodeFn = createServerFn({ method: 'POST' })
127
130
  .validator(idSchema)
128
131
  .handler(async ({ data }) => {
132
+ const prisma = await getPrisma()
129
133
  await requireAdmin()
130
134
  const row = await prisma.invitationCode.findUnique({
131
135
  where: { id: data.id },
@@ -140,6 +144,7 @@ export const deleteInvitationCodeFn = createServerFn({ method: 'POST' })
140
144
  export const getRegistrationModeFn = createServerFn({ method: 'GET' })
141
145
  .validator(z.object({}))
142
146
  .handler(async () => {
147
+ const prisma = await getPrisma()
143
148
  await requireAdmin()
144
149
  const setting = await prisma.systemSetting.findUnique({
145
150
  where: { key: 'auth.registrationMode' },
@@ -150,6 +155,7 @@ export const getRegistrationModeFn = createServerFn({ method: 'GET' })
150
155
  export const setRegistrationModeFn = createServerFn({ method: 'POST' })
151
156
  .validator(z.object({ mode: z.enum(['invite', 'open']) }))
152
157
  .handler(async ({ data }) => {
158
+ const prisma = await getPrisma()
153
159
  await requireAdmin()
154
160
  await prisma.systemSetting.upsert({
155
161
  where: { key: 'auth.registrationMode' },
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
22
  import { bookmarkCanonicalId, parseGithubUrl } from '@/server/knowledge-public/canonical-id'
23
23
 
@@ -32,6 +32,7 @@ const createSchema = z.object({
32
32
  export const adminCreateBookmarkFn = createServerFn({ method: 'POST' })
33
33
  .validator(createSchema)
34
34
  .handler(async ({ data }) => {
35
+ const prisma = await getPrisma()
35
36
  const session = await requireAdmin()
36
37
  const gh = parseGithubUrl(data.url)
37
38
  return prisma.websiteBookmark.create({
@@ -17,12 +17,13 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
22
 
23
23
  export const adminDeleteBookmarkFn = createServerFn({ method: 'POST' })
24
24
  .validator(z.object({ id: z.string() }))
25
25
  .handler(async ({ data }) => {
26
+ const prisma = await getPrisma()
26
27
  await requireAdmin()
27
28
  const existing = await prisma.websiteBookmark.findUnique({ where: { id: data.id } })
28
29
  if (!existing) throw new Error('未找到')
@@ -17,12 +17,13 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
22
 
23
23
  export const adminGetBookmarkDetailFn = createServerFn({ method: 'GET' })
24
24
  .validator(z.object({ id: z.string() }))
25
25
  .handler(async ({ data }) => {
26
+ const prisma = await getPrisma()
26
27
  await requireAdmin()
27
28
  const item = await prisma.websiteBookmark.findFirst({
28
29
  where: { id: data.id, visibility: 'public', isDeleted: false },
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
22
 
23
23
  const listSchema = z.object({
@@ -31,6 +31,7 @@ const listSchema = z.object({
31
31
  export const adminListBookmarksFn = createServerFn({ method: 'GET' })
32
32
  .validator(listSchema)
33
33
  .handler(async ({ data }) => {
34
+ const prisma = await getPrisma()
34
35
  await requireAdmin()
35
36
  const where: any = { visibility: 'public', isDeleted: false }
36
37
  if (data.search) {
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
22
  import { syncBookmarkGithub } from '@/server/knowledge-public/github-sync'
23
23
 
@@ -28,6 +28,7 @@ const syncSchema = z.object({
28
28
  export const adminSyncAllGithubFn = createServerFn({ method: 'POST' })
29
29
  .validator(syncSchema)
30
30
  .handler(async ({ data }) => {
31
+ const prisma = await getPrisma()
31
32
  await requireAdmin()
32
33
  const where: any = { isGithub: true, isDeleted: false }
33
34
  if (!data.force) {
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
- import { prisma } from '@allbluecn/database'
20
+ import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
22
  import { bookmarkCanonicalId, parseGithubUrl } from '@/server/knowledge-public/canonical-id'
23
23
 
@@ -33,6 +33,7 @@ const updateSchema = z.object({
33
33
  export const adminUpdateBookmarkFn = createServerFn({ method: 'POST' })
34
34
  .validator(updateSchema)
35
35
  .handler(async ({ data }) => {
36
+ const prisma = await getPrisma()
36
37
  await requireAdmin()
37
38
  const { id, ...rest } = data
38
39
  const existing = await prisma.websiteBookmark.findUnique({ where: { id } })