@allbluecn/admin-app 0.3.1 → 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 (199) 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/paraglide/README.md +1 -1
  57. package/src/providers/index.tsx +1 -1
  58. package/src/routeTree.gen.ts +67 -78
  59. package/src/routes/__root.tsx +4 -4
  60. package/src/routes/_nav/audit/route.lazy.tsx +1 -2
  61. package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
  62. package/src/routes/_nav/components/route.lazy.tsx +14 -34
  63. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
  64. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
  65. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
  66. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
  67. package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
  68. package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
  69. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
  70. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
  71. package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
  72. package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
  73. package/src/routes/_nav/monitoring/health/route.tsx +14 -0
  74. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
  75. package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
  76. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
  77. package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
  78. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
  79. package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
  80. package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
  81. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
  82. package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
  83. package/src/routes/_nav/permissions/route.tsx +1 -1
  84. package/src/routes/_nav/route.tsx +5 -13
  85. package/src/routes/_nav/sso/route.lazy.tsx +2 -3
  86. package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
  87. package/src/routes/_nav/users/-components/users-table.tsx +1 -1
  88. package/src/routes/_nav/users/route.lazy.tsx +1 -1
  89. package/src/routes/login/route.lazy.tsx +1 -1
  90. package/src/routes/setup/route.lazy.tsx +2 -2
  91. package/src/server/__tests__/admin-billing.test.ts +191 -16
  92. package/src/server/__tests__/compliance.test.ts +44 -7
  93. package/src/server/__tests__/permissions.test.ts +10 -5
  94. package/src/server/__tests__/prompt-defaults.test.ts +3 -3
  95. package/src/server/admin-ai-alerts.ts +2 -1
  96. package/src/server/admin-ai-usage.ts +2 -1
  97. package/src/server/ai-memory/store.ts +6 -2
  98. package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
  99. package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
  100. package/src/server/knowledge-public/github-sync.ts +2 -1
  101. package/src/server/license-issue.ts +2 -1
  102. package/src/server/middlewares/auth-rate-limit.ts +117 -0
  103. package/src/server/observability/health-check.ts +4 -1
  104. package/src/server/observability/metrics.ts +17 -15
  105. package/src/server/observability/request-middleware.ts +2 -1
  106. package/src/server/observability/timing.ts +14 -16
  107. package/src/server/serverFns/admin-billing.ts +107 -12
  108. package/src/server/serverFns/ai-memory/list-users.ts +2 -1
  109. package/src/server/serverFns/ai-prompts/activate.ts +2 -1
  110. package/src/server/serverFns/ai-prompts/get.ts +3 -2
  111. package/src/server/serverFns/ai-prompts/list.ts +3 -2
  112. package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
  113. package/src/server/serverFns/ai-prompts/save.ts +5 -3
  114. package/src/server/serverFns/ai-provider/create.ts +4 -1
  115. package/src/server/serverFns/ai-provider/delete.ts +4 -1
  116. package/src/server/serverFns/ai-provider/get.ts +4 -1
  117. package/src/server/serverFns/ai-provider/list.ts +28 -1
  118. package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
  119. package/src/server/serverFns/ai-provider/update.ts +4 -1
  120. package/src/server/serverFns/anyhub/market.ts +8 -1
  121. package/src/server/serverFns/audit.ts +14 -1
  122. package/src/server/serverFns/auth/setup.ts +3 -1
  123. package/src/server/serverFns/compliance.ts +32 -3
  124. package/src/server/serverFns/components/import.ts +69 -14
  125. package/src/server/serverFns/components/list.ts +4 -1
  126. package/src/server/serverFns/components/mutations.ts +9 -1
  127. package/src/server/serverFns/dependency/list.ts +28 -5
  128. package/src/server/serverFns/dependency/sync.ts +7 -2
  129. package/src/server/serverFns/dependency/update.ts +4 -1
  130. package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
  131. package/src/server/serverFns/developer-logs.ts +5 -1
  132. package/src/server/serverFns/invitations.ts +7 -1
  133. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
  134. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
  135. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
  136. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
  137. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
  138. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
  139. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  140. package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
  141. package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
  142. package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
  143. package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
  144. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  145. package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
  146. package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
  147. package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
  148. package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
  149. package/src/server/serverFns/licenses.ts +8 -2
  150. package/src/server/serverFns/monitoring/get-errors.ts +2 -1
  151. package/src/server/serverFns/monitoring/get-health.ts +2 -1
  152. package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
  153. package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
  154. package/src/server/serverFns/monitoring/report-error.ts +2 -1
  155. package/src/server/serverFns/monitoring/reset.ts +2 -1
  156. package/src/server/serverFns/oauth-providers.ts +4 -1
  157. package/src/server/serverFns/permissions.ts +18 -11
  158. package/src/server/serverFns/sentry/config.ts +11 -8
  159. package/src/server/serverFns/sso-config.ts +17 -2
  160. package/src/server/serverFns/user/create.ts +6 -1
  161. package/src/server/serverFns/user/delete.ts +6 -1
  162. package/src/server/serverFns/user/detail.ts +5 -2
  163. package/src/server/serverFns/user/list.ts +19 -4
  164. package/src/server/serverFns/user/reset-password.ts +10 -2
  165. package/src/server/serverFns/user/update-role.ts +10 -2
  166. package/src/server/serverFns/user/update-status.ts +10 -2
  167. package/src/styles/globals.css +4 -0
  168. package/src/components/data-table/data-table-pagination.tsx +0 -112
  169. package/src/components/data-table/data-table.tsx +0 -218
  170. package/src/components/data-table/table-detail-layout.tsx +0 -98
  171. package/src/components/ui/alert-dialog.tsx +0 -187
  172. package/src/components/ui/alert.tsx +0 -75
  173. package/src/components/ui/avatar.tsx +0 -106
  174. package/src/components/ui/badge.tsx +0 -51
  175. package/src/components/ui/button.tsx +0 -57
  176. package/src/components/ui/card.tsx +0 -102
  177. package/src/components/ui/checkbox.tsx +0 -28
  178. package/src/components/ui/collapsible.tsx +0 -19
  179. package/src/components/ui/dialog.tsx +0 -155
  180. package/src/components/ui/dropdown-menu.tsx +0 -267
  181. package/src/components/ui/field.tsx +0 -236
  182. package/src/components/ui/input-group.tsx +0 -158
  183. package/src/components/ui/input.tsx +0 -19
  184. package/src/components/ui/label.tsx +0 -17
  185. package/src/components/ui/pagination.tsx +0 -130
  186. package/src/components/ui/select.tsx +0 -234
  187. package/src/components/ui/separator.tsx +0 -22
  188. package/src/components/ui/sheet.tsx +0 -135
  189. package/src/components/ui/sidebar.tsx +0 -721
  190. package/src/components/ui/skeleton.tsx +0 -13
  191. package/src/components/ui/switch.tsx +0 -29
  192. package/src/components/ui/table.tsx +0 -115
  193. package/src/components/ui/textarea.tsx +0 -17
  194. package/src/components/ui/toast.tsx +0 -282
  195. package/src/components/ui/tooltip.tsx +0 -63
  196. package/src/hooks/use-mobile.ts +0 -19
  197. package/src/lib/avatar.ts +0 -41
  198. /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
  199. /package/src/routes/{api.sentry-test.ts → api/sentry-test.ts} +0 -0
@@ -95,7 +95,7 @@ describe('listComplianceExportsFn(admin 合规导出任务列表)', () => {
95
95
  expect(complianceExport.findMany).not.toHaveBeenCalled()
96
96
  })
97
97
 
98
- it('Pro 服务缺省时走 prisma 分页查询,skip/take 按页计算', async () => {
98
+ it('Pro 服务缺省时走 prisma 分页查询,skip/take 按页计算 + 白名单 select', async () => {
99
99
  const items = [{ id: 'e-1' }]
100
100
  complianceExport.findMany.mockResolvedValue(items)
101
101
  complianceExport.count.mockResolvedValue(42)
@@ -103,25 +103,62 @@ describe('listComplianceExportsFn(admin 合规导出任务列表)', () => {
103
103
  const result = await listComplianceExportsFn({ data: { page: 3, pageSize: 10 } })
104
104
 
105
105
  expect(complianceExport.findMany).toHaveBeenCalledWith({
106
+ where: {},
106
107
  orderBy: { createdAt: 'desc' },
107
108
  skip: 20,
108
109
  take: 10,
110
+ select: {
111
+ id: true,
112
+ type: true,
113
+ scope: true,
114
+ targetId: true,
115
+ status: true,
116
+ expiresAt: true,
117
+ error: true,
118
+ requestedBy: true,
119
+ createdAt: true,
120
+ },
109
121
  })
110
- expect(complianceExport.count).toHaveBeenCalledWith()
122
+ expect(complianceExport.count).toHaveBeenCalledWith({ where: {} })
111
123
  expect(result).toEqual({ items, total: 42 })
112
124
  })
113
125
 
126
+ it('DB-02:status 过滤参数化,where 同时作用于 findMany 与 count', async () => {
127
+ complianceExport.findMany.mockResolvedValue([])
128
+ complianceExport.count.mockResolvedValue(0)
129
+
130
+ await listComplianceExportsFn({ data: { page: 1, pageSize: 20, status: 'pending' } })
131
+
132
+ const arg = complianceExport.findMany.mock.calls[0][0]
133
+ expect(arg.where).toEqual({ status: 'pending' })
134
+ expect(complianceExport.count).toHaveBeenCalledWith({ where: { status: 'pending' } })
135
+ })
136
+
137
+ it('DB-05:select 白名单排除 content 大字段与 downloadUrl', async () => {
138
+ complianceExport.findMany.mockResolvedValue([])
139
+ complianceExport.count.mockResolvedValue(0)
140
+
141
+ await listComplianceExportsFn({ data: {} })
142
+
143
+ const arg = complianceExport.findMany.mock.calls[0][0]
144
+ expect(arg.select).not.toHaveProperty('content')
145
+ expect(arg.select).not.toHaveProperty('downloadUrl')
146
+ expect(arg.select).not.toHaveProperty('completedAt')
147
+ })
148
+
114
149
  it('未传分页参数时使用默认值(page=1, pageSize=20)', async () => {
115
150
  complianceExport.findMany.mockResolvedValue([])
116
151
  complianceExport.count.mockResolvedValue(0)
117
152
 
118
153
  await listComplianceExportsFn({ data: {} })
119
154
 
120
- expect(complianceExport.findMany).toHaveBeenCalledWith({
121
- orderBy: { createdAt: 'desc' },
122
- skip: 0,
123
- take: 20,
124
- })
155
+ expect(complianceExport.findMany).toHaveBeenCalledWith(
156
+ expect.objectContaining({
157
+ orderBy: { createdAt: 'desc' },
158
+ skip: 0,
159
+ take: 20,
160
+ }),
161
+ )
125
162
  })
126
163
 
127
164
  it('查询前强制管理员身份与合规功能授权', async () => {
@@ -51,6 +51,7 @@ vi.mock('@allbluecn/database', () => ({
51
51
  customRole: {
52
52
  count: vi.fn(),
53
53
  create: vi.fn(),
54
+ createMany: vi.fn(),
54
55
  findMany: vi.fn(),
55
56
  findFirst: vi.fn(),
56
57
  delete: vi.fn(),
@@ -101,6 +102,7 @@ import {
101
102
  const customRole = prisma.customRole as unknown as {
102
103
  count: ReturnType<typeof vi.fn>
103
104
  create: ReturnType<typeof vi.fn>
105
+ createMany: ReturnType<typeof vi.fn>
104
106
  findMany: ReturnType<typeof vi.fn>
105
107
  findFirst: ReturnType<typeof vi.fn>
106
108
  delete: ReturnType<typeof vi.fn>
@@ -170,17 +172,20 @@ describe('listRolesFn', () => {
170
172
  expect(roleAssignment.createMany).not.toHaveBeenCalled()
171
173
  })
172
174
 
173
- it('首次种子化创建 5 个系统角色预设', async () => {
175
+ it('首次种子化创建 5 个系统角色预设(DB-01:一次 createMany 替代逐 preset create)', async () => {
174
176
  customRole.count.mockResolvedValue(0)
177
+ customRole.createMany.mockResolvedValue({ count: 5 })
175
178
  services.set('permissions', { kind: 'pro' })
176
179
 
177
180
  await listRolesFn({ data: { workspaceId: WS } })
178
181
 
179
- const names = customRole.create.mock.calls.map((c: any[]) => c[0].data.name)
180
- expect(names).toEqual(['owner', 'admin', 'member', 'editor', 'viewer'])
182
+ expect(customRole.createMany).toHaveBeenCalledTimes(1)
183
+ const rows = customRole.createMany.mock.calls[0][0].data
184
+ expect(rows.map((r: any) => r.name)).toEqual(['owner', 'admin', 'member', 'editor', 'viewer'])
181
185
  // 系统角色必须标记 isSystem,用户不可伪造/删除
182
- expect(customRole.create.mock.calls.every((c: any[]) => c[0].data.isSystem === true)).toBe(true)
183
- expect(customRole.create.mock.calls.every((c: any[]) => c[0].data.workspaceId === WS)).toBe(true)
186
+ expect(rows.every((r: any) => r.isSystem === true)).toBe(true)
187
+ expect(rows.every((r: any) => r.workspaceId === WS)).toBe(true)
188
+ expect(customRole.create).not.toHaveBeenCalled()
184
189
  })
185
190
 
186
191
  it('种子化时给 ADMIN 用户赋 owner 角色', async () => {
@@ -16,7 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { describe, it, expect, vi, beforeEach } from "vitest"
19
- import { type PromptSection } from "@allbluecn/web-app/ai-gateway/prompts/definitions"
19
+ import type { PromptSection } from "@allbluecn/kernel"
20
20
 
21
21
  const mocks = vi.hoisted(() => {
22
22
  const findMany = vi.fn()
@@ -34,9 +34,9 @@ vi.mock("@allbluecn/database", () => ({ prisma: mocks.prisma }))
34
34
 
35
35
  // 内置定义走真实实现:admin 与 web 共享同一份默认提示词,
36
36
  // 这里 mock 掉会掩盖「默认值为空」这类回归。
37
- import { backfillMissingPromptTemplates } from "../serverFns/ai-prompts/prompt-defaults"
37
+ import { backfillMissingPromptTemplates } from "../ai-prompts/prompt-defaults"
38
38
  import { promptSectionSchema } from "../serverFns/ai-prompts/prompt-schemas"
39
- import { listPromptDefinitions } from "@allbluecn/web-app/ai-gateway/prompts/definitions"
39
+ import { listPromptDefinitions } from "@allbluecn/kernel"
40
40
 
41
41
  interface BackfillRow {
42
42
  key: string
@@ -1,4 +1,4 @@
1
- import { prisma } from '@allbluecn/database'
1
+ import { getPrisma } from '@allbluecn/database/get-prisma'
2
2
 
3
3
  export interface AiAlert {
4
4
  type: 'quota_exhausted' | 'high_cost' | 'error_spike' | 'provider_failure'
@@ -31,6 +31,7 @@ export interface AlertCheckOptions {
31
31
  }
32
32
 
33
33
  export async function checkAiAlerts(options: AlertCheckOptions = {}): Promise<AiAlert[]> {
34
+ const prisma = await getPrisma()
34
35
  const { hours = 24, errorRateThreshold = 0.05 } = options
35
36
  const dailyCostThresholdUsd =
36
37
  options.dailyCostThresholdUsd ??
@@ -1,4 +1,4 @@
1
- import { prisma } from '@allbluecn/database'
1
+ import { getPrisma } from '@allbluecn/database/get-prisma'
2
2
 
3
3
  export interface AiUsageOverview {
4
4
  totalCalls: number
@@ -24,6 +24,7 @@ export interface AiUsageOverviewOptions {
24
24
  }
25
25
 
26
26
  export async function getAiUsageOverview(options: AiUsageOverviewOptions = {}): Promise<AiUsageOverview> {
27
+ const prisma = await getPrisma()
27
28
  const {
28
29
  days = 30,
29
30
  workspace: workspaceOpt = {},
@@ -16,10 +16,10 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  // apps/admin/src/server/ai-memory/store.ts
19
- import { prisma } from "@allbluecn/database"
19
+ import { getPrisma } from "@allbluecn/database/get-prisma"
20
20
 
21
21
  // memory_facts 是 pgvector 原生表(Prisma 无法表达 vector 类型,同族参考
22
- // knowledge_embeddings / prd_embeddings),由 migration 建表、以 raw SQL 读写。
22
+ // knowledge_embeddings / design_embeddings),由 migration 建表、以 raw SQL 读写。
23
23
  // 桌面端 SQLite 无此表,本模块仅在 Web/Admin 服务端路径使用。
24
24
 
25
25
  export interface MemoryFactRow {
@@ -46,6 +46,7 @@ export async function listMemoryFacts(
46
46
  userId: string,
47
47
  limit = MEMORY_FACTS_LIST_LIMIT,
48
48
  ): Promise<Array<MemoryFactRow>> {
49
+ const prisma = await getPrisma()
49
50
  const rows = (await prisma.$queryRawUnsafe(
50
51
  `SELECT "id", "content", "threadId", "createdAt"
51
52
  FROM "memory_facts"
@@ -75,6 +76,7 @@ export async function listMemoryFacts(
75
76
  export async function countMemoryFacts(
76
77
  userId: string,
77
78
  ): Promise<MemoryStats> {
79
+ const prisma = await getPrisma()
78
80
  const rows = (await prisma.$queryRawUnsafe(
79
81
  `SELECT COUNT(*)::int AS "c", MAX("createdAt") AS "lastWriteAt"
80
82
  FROM "memory_facts"
@@ -89,6 +91,7 @@ export async function countMemoryFacts(
89
91
  }
90
92
 
91
93
  export async function deleteMemoryFact(id: string): Promise<number> {
94
+ const prisma = await getPrisma()
92
95
  return prisma.$executeRawUnsafe(
93
96
  `DELETE FROM "memory_facts" WHERE "id" = $1`,
94
97
  id,
@@ -96,6 +99,7 @@ export async function deleteMemoryFact(id: string): Promise<number> {
96
99
  }
97
100
 
98
101
  export async function clearMemoryFacts(userId: string): Promise<number> {
102
+ const prisma = await getPrisma()
99
103
  return prisma.$executeRawUnsafe(
100
104
  `DELETE FROM "memory_facts" WHERE "userId" = $1`,
101
105
  userId,
@@ -15,9 +15,10 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- // apps/admin/src/server/serverFns/ai-prompts/prompt-defaults.ts
19
- import { prisma, type Prisma } from "@allbluecn/database"
20
- import { listPromptDefinitions } from "@allbluecn/web-app/ai-gateway/prompts/definitions"
18
+ // apps/admin/src/server/ai-prompts/prompt-defaults.ts
19
+ import { getPrisma } from "@allbluecn/database/get-prisma"
20
+ import type { Prisma } from "@allbluecn/database"
21
+ import { listPromptDefinitions } from "@allbluecn/kernel"
21
22
 
22
23
  // 提示词的唯一来源是代码内置定义(apps/web ai-gateway/prompts/builtin.ts);
23
24
  // admin 后台只是查看与二次编辑的入口,DB 永远不该是「唯一事实」——
@@ -34,6 +35,7 @@ function toJson(value: unknown): Prisma.InputJsonValue {
34
35
  }
35
36
 
36
37
  export async function backfillMissingPromptTemplates() {
38
+ const prisma = await getPrisma()
37
39
  const defaults = listPromptDefinitions()
38
40
  const existing = await prisma.systemPromptTemplate.findMany({
39
41
  select: { key: true },
@@ -15,11 +15,11 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- // apps/admin/src/server/serverFns/dependency/helpers.ts
18
+ // apps/admin/src/server/dependency/helpers.ts
19
19
  import { existsSync } from "node:fs"
20
20
  import { readFile } from "node:fs/promises"
21
21
  import path from "node:path"
22
- import { prisma } from "@allbluecn/database"
22
+ import { getPrisma } from "@allbluecn/database/get-prisma"
23
23
  import {
24
24
  DEFAULT_WATCHED_DEPENDENCIES,
25
25
  type DependencyUsage,
@@ -123,7 +123,7 @@ export async function queryNpmRegistry(
123
123
  /** 从 repository URL 解析 GitHub owner/repo */
124
124
  function parseGitHubRepo(repoUrl: string): { owner: string; repo: string } | null {
125
125
  // 匹配 github.com/owner/repo(容忍 git+ 前缀、.git 后缀、ssh 协议)
126
- const m = repoUrl.match(/github\.com[:/]([^/]+)\/([^/\.]+)/i)
126
+ const m = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/i)
127
127
  if (!m) return null
128
128
  return { owner: m[1], repo: m[2] }
129
129
  }
@@ -212,6 +212,7 @@ export function pickCurrentVersion(usages: DependencyUsage[]): string | null {
212
212
  /** 首次访问时 seed 预设关注列表;若已 seed 则回填缺失的 description(升级兼容) */
213
213
  let seeded = false
214
214
  export async function ensureSeedData(): Promise<void> {
215
+ const prisma = await getPrisma()
215
216
  if (seeded) return
216
217
  const count = await prisma.thirdPartyDependency.count()
217
218
  if (count === 0) {
@@ -15,7 +15,7 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { prisma } from '@allbluecn/database'
18
+ import { getPrisma } from '@allbluecn/database/get-prisma'
19
19
 
20
20
  export interface GithubData {
21
21
  stars: number
@@ -63,6 +63,7 @@ export async function fetchGithubData(
63
63
  }
64
64
 
65
65
  export async function syncBookmarkGithub(bookmarkId: string): Promise<boolean> {
66
+ const prisma = await getPrisma()
66
67
  const bm = await prisma.websiteBookmark.findUnique({
67
68
  where: { id: bookmarkId },
68
69
  select: { id: true, isGithub: true, githubOwner: true, githubRepo: true },
@@ -15,7 +15,7 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { prisma } from '@allbluecn/database'
18
+ import { getPrisma } from '@allbluecn/database/get-prisma'
19
19
  import { getProService } from '@/server/pro-services'
20
20
  import { FeatureRegistry } from '@allbluecn/kernel'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
@@ -29,6 +29,7 @@ export const issueLicense = async (data: {
29
29
  features: Record<string, boolean>
30
30
  expiresAt: string | null
31
31
  }) => {
32
+ const prisma = await getPrisma()
32
33
  const session = await requireAdmin()
33
34
 
34
35
  const unknownKeys = Object.keys(data.features).filter(
@@ -0,0 +1,117 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ /**
19
+ * 认证类端点失败限流(SEC-09 登录 / SEC-10 分享密码)——进程内存态固定窗口。
20
+ *
21
+ * ⚠️ 架构边界:计数器存于单进程内存(Map),**多实例部署不共享**,
22
+ * 实际阈值 = 配置值 × 实例数。本期不实施分布式限流(Redis 等),
23
+ * 多实例部署落地前不得将本模块视为唯一防线。
24
+ *
25
+ * 设计要点:
26
+ * - 仅失败尝试计数(登录:用户不存在/密码错误;分享密码:校验失败),
27
+ * 成功后清零——正常用户与自动化 e2e(成功登录)不受影响
28
+ * - key 归一化:email 小写去空白,避免大小写变体绕过;IP 取不到时归入
29
+ * `unknown` 桶(按 email 维度仍受限流保护)
30
+ * - 测试环境豁免:NODE_ENV=test(vitest)时不计数不拦截,防单测互踩
31
+ * - 账号状态类失败(封禁/未激活)不计入——非口令爆破向量
32
+ */
33
+
34
+ const store = new Map<string, { count: number; resetAt: number }>()
35
+
36
+ export const LOGIN_RATE_LIMIT = { max: 5, windowMs: 15 * 60 * 1000 } as const
37
+ export const SHARE_PASSWORD_RATE_LIMIT = { max: 10, windowMs: 15 * 60 * 1000 } as const
38
+
39
+ const CLEANUP_INTERVAL = 60_000
40
+ const cleanupTimer = setInterval(() => {
41
+ const now = Date.now()
42
+ for (const [key, entry] of store) {
43
+ if (now > entry.resetAt) store.delete(key)
44
+ }
45
+ }, CLEANUP_INTERVAL)
46
+ // 防止 timer 阻止进程退出(jsdom 环境 setInterval 返回 number,无 unref)
47
+ cleanupTimer.unref?.()
48
+
49
+ /** 测试环境豁免(vitest 注入 NODE_ENV=test) */
50
+ export function isAuthRateLimitBypassed(): boolean {
51
+ return process.env.NODE_ENV === 'test'
52
+ }
53
+
54
+ function isBlocked(key: string, max: number): boolean {
55
+ if (isAuthRateLimitBypassed()) return false
56
+ const entry = store.get(key)
57
+ if (!entry) return false
58
+ if (Date.now() > entry.resetAt) {
59
+ store.delete(key)
60
+ return false
61
+ }
62
+ return entry.count >= max
63
+ }
64
+
65
+ function recordFailure(key: string, windowMs: number): void {
66
+ if (isAuthRateLimitBypassed()) return
67
+ const now = Date.now()
68
+ const entry = store.get(key)
69
+ if (!entry || now > entry.resetAt) {
70
+ store.set(key, { count: 1, resetAt: now + windowMs })
71
+ return
72
+ }
73
+ entry.count++
74
+ }
75
+
76
+ function loginKey(email: string, ip: string | null): string {
77
+ return `login:${email.toLowerCase().trim()}:${ip ?? 'unknown'}`
78
+ }
79
+
80
+ /** 登录是否已被限流(email+IP 维度)。被限流时调用方应返回与密码错误不可区分的失败。 */
81
+ export function isLoginBlocked(email: string, ip: string | null): boolean {
82
+ return isBlocked(loginKey(email, ip), LOGIN_RATE_LIMIT.max)
83
+ }
84
+
85
+ /** 记录一次登录失败(用户不存在或密码错误) */
86
+ export function recordLoginFailure(email: string, ip: string | null): void {
87
+ recordFailure(loginKey(email, ip), LOGIN_RATE_LIMIT.windowMs)
88
+ }
89
+
90
+ /** 登录成功后清零失败计数 */
91
+ export function clearLoginFailures(email: string, ip: string | null): void {
92
+ store.delete(loginKey(email, ip))
93
+ }
94
+
95
+ function sharePasswordKey(ip: string | null): string {
96
+ return `sharePwd:${ip ?? 'unknown'}`
97
+ }
98
+
99
+ /** 分享密码校验是否已被限流(IP 维度)。被限流时调用方应返回 valid:false(不泄露限流细节)。 */
100
+ export function isSharePasswordBlocked(ip: string | null): boolean {
101
+ return isBlocked(sharePasswordKey(ip), SHARE_PASSWORD_RATE_LIMIT.max)
102
+ }
103
+
104
+ /** 记录一次分享密码校验失败 */
105
+ export function recordSharePasswordFailure(ip: string | null): void {
106
+ recordFailure(sharePasswordKey(ip), SHARE_PASSWORD_RATE_LIMIT.windowMs)
107
+ }
108
+
109
+ /** 分享密码校验成功后清零失败计数 */
110
+ export function clearSharePasswordFailures(ip: string | null): void {
111
+ store.delete(sharePasswordKey(ip))
112
+ }
113
+
114
+ /** 测试辅助:清空整个 store(用例隔离用,生产勿调) */
115
+ export function resetAuthRateLimitStoreForTest(): void {
116
+ store.clear()
117
+ }
@@ -16,7 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import os from 'node:os'
19
- import { prisma } from '@allbluecn/database'
19
+ import { getPrisma } from '@allbluecn/database/get-prisma'
20
20
 
21
21
  interface HealthResult {
22
22
  status: 'healthy' | 'unhealthy'
@@ -35,6 +35,7 @@ let healthTimer: ReturnType<typeof setInterval> | null = null
35
35
  let healthEnabled = true
36
36
 
37
37
  export async function performHealthCheck(): Promise<HealthResult> {
38
+ const prisma = await getPrisma()
38
39
  let dbResult: { status: 'connected' | 'error'; latency?: number; error?: string }
39
40
 
40
41
  try {
@@ -113,6 +114,7 @@ export function isHealthCheckEnabled(): boolean {
113
114
  }
114
115
 
115
116
  export async function setHealthCheckEnabled(enabled: boolean): Promise<void> {
117
+ const prisma = await getPrisma()
116
118
  if (enabled) {
117
119
  startHealthCheckTimer()
118
120
  } else {
@@ -132,6 +134,7 @@ export async function setHealthCheckEnabled(enabled: boolean): Promise<void> {
132
134
  }
133
135
 
134
136
  export async function getHealthCheckEnabled(): Promise<boolean> {
137
+ const prisma = await getPrisma()
135
138
  const setting = await prisma.systemSetting.findUnique({
136
139
  where: { key: 'monitoring_health_check_enabled' },
137
140
  })
@@ -15,7 +15,7 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { prisma } from '@allbluecn/database'
18
+ import { getPrisma } from '@allbluecn/database/get-prisma'
19
19
 
20
20
  interface MetricStats {
21
21
  count: number
@@ -76,21 +76,23 @@ class MetricsCollector {
76
76
  }
77
77
 
78
78
  async flushToDb(): Promise<void> {
79
+ const prisma = await getPrisma()
79
80
  const allStats = this.getAllStats()
80
- for (const [name, stats] of Object.entries(allStats)) {
81
- await prisma.monitoringMetricsSnapshot.create({
82
- data: {
83
- metricName: name,
84
- count: stats.count,
85
- avg: stats.avg,
86
- p50: stats.p50,
87
- p95: stats.p95,
88
- p99: stats.p99,
89
- min: stats.min,
90
- max: stats.max,
91
- },
92
- })
93
- }
81
+ // DB-01/DB-06:快照落盘从逐指标 create(N 次串行查询)改为 1 次 createMany(返回值不被消费)
82
+ // 查询次数 before: N → after: 1
83
+ if (Object.keys(allStats).length === 0) return
84
+ await prisma.monitoringMetricsSnapshot.createMany({
85
+ data: Object.entries(allStats).map(([name, stats]) => ({
86
+ metricName: name,
87
+ count: stats.count,
88
+ avg: stats.avg,
89
+ p50: stats.p50,
90
+ p95: stats.p95,
91
+ p99: stats.p99,
92
+ min: stats.min,
93
+ max: stats.max,
94
+ })),
95
+ })
94
96
  }
95
97
 
96
98
  reset(): void {
@@ -16,7 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { createMiddleware } from '@tanstack/react-start'
19
- import { prisma } from '@allbluecn/database'
19
+ import { getPrisma } from '@allbluecn/database/get-prisma'
20
20
  import { metrics } from './metrics'
21
21
  import { logger } from './logger'
22
22
  import { reportError } from './error-reporter'
@@ -111,6 +111,7 @@ export const observabilityMiddleware = createMiddleware().server(async ({ reques
111
111
  })
112
112
 
113
113
  async function saveRequestLogAsync(entry: RequestLogEntry): Promise<void> {
114
+ const prisma = await getPrisma()
114
115
  await prisma.monitoringRequestLog.create({
115
116
  data: {
116
117
  timestamp: new Date(entry.timestamp),
@@ -23,20 +23,18 @@ export interface TimingResult {
23
23
  label: string
24
24
  }
25
25
 
26
- export function timed(label: string, fn: () => Promise<unknown> | unknown): Promise<TimingResult & { data: unknown }> {
27
- return new Promise(async (resolve, reject) => {
28
- const startTime = Date.now()
29
- try {
30
- const data = await fn()
31
- const duration = Date.now() - startTime
32
- metrics.recordTiming(label, duration)
33
- logger('info', `Timed operation`, { label, duration })
34
- resolve({ label, duration, data })
35
- } catch (error) {
36
- const duration = Date.now() - startTime
37
- metrics.recordTiming(label, duration)
38
- logger('error', `Timed operation failed`, { label, duration, error: (error as Error).message })
39
- reject(error)
40
- }
41
- })
26
+ export async function timed(label: string, fn: () => Promise<unknown> | unknown): Promise<TimingResult & { data: unknown }> {
27
+ const startTime = Date.now()
28
+ try {
29
+ const data = await fn()
30
+ const duration = Date.now() - startTime
31
+ metrics.recordTiming(label, duration)
32
+ logger('info', `Timed operation`, { label, duration })
33
+ return { label, duration, data }
34
+ } catch (error) {
35
+ const duration = Date.now() - startTime
36
+ metrics.recordTiming(label, duration)
37
+ logger('error', `Timed operation failed`, { label, duration, error: (error as Error).message })
38
+ throw error
39
+ }
42
40
  }