@allbluecn/admin-app 0.1.0

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 (276) hide show
  1. package/package.json +86 -0
  2. package/src/client.tsx +31 -0
  3. package/src/components/admin/admin-user-menu.tsx +90 -0
  4. package/src/components/admin/developer-log.tsx +356 -0
  5. package/src/components/ai-provider/model-list-editor.tsx +147 -0
  6. package/src/components/ai-provider/provider-form.tsx +434 -0
  7. package/src/components/ai-provider/provider-icon.tsx +43 -0
  8. package/src/components/ai-provider/provider-logo.tsx +87 -0
  9. package/src/components/ai-provider/provider-table.tsx +258 -0
  10. package/src/components/auth/admin-login-form.tsx +214 -0
  11. package/src/components/auth/auth-brand-panel.tsx +60 -0
  12. package/src/components/auth/auth-header.tsx +38 -0
  13. package/src/components/auth/auth-layout.tsx +66 -0
  14. package/src/components/auth/auth-providers.tsx +34 -0
  15. package/src/components/auth/icon-input.tsx +46 -0
  16. package/src/components/auth/oauth-section.tsx +51 -0
  17. package/src/components/auth/password-input.tsx +67 -0
  18. package/src/components/common/page-header.tsx +42 -0
  19. package/src/components/common/placeholder-page.tsx +42 -0
  20. package/src/components/component/component-form-dialog.tsx +348 -0
  21. package/src/components/component/component-import-dialog.tsx +235 -0
  22. package/src/components/component/component-table.tsx +153 -0
  23. package/src/components/data-table/data-table-pagination.tsx +114 -0
  24. package/src/components/data-table/data-table.tsx +218 -0
  25. package/src/components/data-table/table-detail-layout.tsx +99 -0
  26. package/src/components/dependency/changelog-dialog.tsx +108 -0
  27. package/src/components/dependency/columns.tsx +232 -0
  28. package/src/components/dependency/dependency-table.tsx +368 -0
  29. package/src/components/dependency/edit-dialog.tsx +200 -0
  30. package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
  31. package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
  32. package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
  33. package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
  34. package/src/components/layout/sidebar-02/index.tsx +33 -0
  35. package/src/components/layout/sidebar-02/logo.tsx +40 -0
  36. package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
  37. package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
  38. package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
  39. package/src/components/magicui/animated-theme-toggler.tsx +272 -0
  40. package/src/components/magicui/rainbow-button.tsx +78 -0
  41. package/src/components/market/columns.tsx +151 -0
  42. package/src/components/market/market-app-form.tsx +237 -0
  43. package/src/components/market/market-app-list.tsx +156 -0
  44. package/src/components/monitoring/error-list.tsx +143 -0
  45. package/src/components/monitoring/health-cards.tsx +152 -0
  46. package/src/components/monitoring/metrics-stats.tsx +184 -0
  47. package/src/components/monitoring/request-log-table.tsx +179 -0
  48. package/src/components/providers/theme-toggle.tsx +59 -0
  49. package/src/components/settings/avatar/avatar-config.ts +46 -0
  50. package/src/components/ui/alert-dialog.tsx +216 -0
  51. package/src/components/ui/alert.tsx +76 -0
  52. package/src/components/ui/avatar.tsx +129 -0
  53. package/src/components/ui/badge.tsx +66 -0
  54. package/src/components/ui/button.tsx +84 -0
  55. package/src/components/ui/card.tsx +120 -0
  56. package/src/components/ui/checkbox.tsx +50 -0
  57. package/src/components/ui/collapsible.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +185 -0
  59. package/src/components/ui/dropdown-menu.tsx +286 -0
  60. package/src/components/ui/field.tsx +255 -0
  61. package/src/components/ui/input-group.tsx +171 -0
  62. package/src/components/ui/input.tsx +36 -0
  63. package/src/components/ui/label.tsx +41 -0
  64. package/src/components/ui/pagination.tsx +146 -0
  65. package/src/components/ui/select.tsx +209 -0
  66. package/src/components/ui/separator.tsx +45 -0
  67. package/src/components/ui/sheet.tsx +164 -0
  68. package/src/components/ui/sidebar.tsx +719 -0
  69. package/src/components/ui/skeleton.tsx +30 -0
  70. package/src/components/ui/sonner.tsx +37 -0
  71. package/src/components/ui/switch.tsx +48 -0
  72. package/src/components/ui/table.tsx +133 -0
  73. package/src/components/ui/textarea.tsx +35 -0
  74. package/src/components/ui/tooltip.tsx +74 -0
  75. package/src/components/user/columns.tsx +153 -0
  76. package/src/components/user/settings-activity-log.tsx +358 -0
  77. package/src/components/user/settings-profile/constants.ts +42 -0
  78. package/src/components/user/settings-profile/types.ts +47 -0
  79. package/src/components/user/settings-profile.tsx +664 -0
  80. package/src/components/user/user-create-dialog.tsx +137 -0
  81. package/src/components/user/user-filters.tsx +117 -0
  82. package/src/hooks/use-mobile.ts +36 -0
  83. package/src/instrument.client.ts +34 -0
  84. package/src/lib/anyhub/capabilities.ts +59 -0
  85. package/src/lib/anyhub/types.ts +43 -0
  86. package/src/lib/auth-client.ts +83 -0
  87. package/src/lib/auth-utils.ts +24 -0
  88. package/src/lib/auth.config.ts +264 -0
  89. package/src/lib/avatar.ts +41 -0
  90. package/src/lib/i18n/client.ts +32 -0
  91. package/src/lib/i18n/config.ts +40 -0
  92. package/src/lib/i18n/detect.ts +43 -0
  93. package/src/lib/i18n/locales/en/admin-ai.json +128 -0
  94. package/src/lib/i18n/locales/en/admin-billing.json +56 -0
  95. package/src/lib/i18n/locales/en/admin-users.json +88 -0
  96. package/src/lib/i18n/locales/en/common.json +877 -0
  97. package/src/lib/i18n/locales/en/enterprise.json +208 -0
  98. package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
  99. package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
  100. package/src/lib/i18n/locales/zh/admin-users.json +88 -0
  101. package/src/lib/i18n/locales/zh/common.json +877 -0
  102. package/src/lib/i18n/locales/zh/enterprise.json +208 -0
  103. package/src/lib/i18n/server.ts +18 -0
  104. package/src/lib/i18n/types.ts +5 -0
  105. package/src/lib/monitoring-benchmarks.ts +85 -0
  106. package/src/lib/monitoring-descriptions.ts +78 -0
  107. package/src/lib/prisma.ts +18 -0
  108. package/src/lib/router-context.ts +38 -0
  109. package/src/lib/session.ts +38 -0
  110. package/src/lib/utils.ts +23 -0
  111. package/src/providers/index.tsx +45 -0
  112. package/src/routeTree.gen.ts +1199 -0
  113. package/src/router.tsx +51 -0
  114. package/src/routes/__root.tsx +170 -0
  115. package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
  116. package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
  117. package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
  118. package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
  119. package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
  120. package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
  121. package/src/routes/_nav/ai-providers/route.tsx +31 -0
  122. package/src/routes/_nav/apps/route.lazy.tsx +30 -0
  123. package/src/routes/_nav/apps/route.tsx +20 -0
  124. package/src/routes/_nav/audit/route.lazy.tsx +100 -0
  125. package/src/routes/_nav/audit/route.tsx +25 -0
  126. package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
  127. package/src/routes/_nav/billing/invoices/route.tsx +25 -0
  128. package/src/routes/_nav/billing/route.lazy.tsx +48 -0
  129. package/src/routes/_nav/billing/route.tsx +27 -0
  130. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
  131. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
  132. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
  133. package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
  134. package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
  135. package/src/routes/_nav/compliance/route.tsx +25 -0
  136. package/src/routes/_nav/components/route.lazy.tsx +184 -0
  137. package/src/routes/_nav/components/route.tsx +40 -0
  138. package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
  139. package/src/routes/_nav/dashboard/route.tsx +20 -0
  140. package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
  141. package/src/routes/_nav/dependencies/route.tsx +32 -0
  142. package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
  143. package/src/routes/_nav/developer-logs/route.tsx +14 -0
  144. package/src/routes/_nav/docs/route.lazy.tsx +30 -0
  145. package/src/routes/_nav/docs/route.tsx +20 -0
  146. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
  147. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
  148. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
  149. package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
  150. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
  151. package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
  152. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
  153. package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
  154. package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
  155. package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
  156. package/src/routes/_nav/licenses/route.tsx +25 -0
  157. package/src/routes/_nav/market/route.lazy.tsx +31 -0
  158. package/src/routes/_nav/market/route.tsx +20 -0
  159. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
  160. package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
  161. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
  162. package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
  163. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
  164. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
  165. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
  166. package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
  167. package/src/routes/_nav/monitoring/route.tsx +20 -0
  168. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
  169. package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
  170. package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
  171. package/src/routes/_nav/permissions/route.tsx +26 -0
  172. package/src/routes/_nav/route.lazy.tsx +31 -0
  173. package/src/routes/_nav/route.tsx +34 -0
  174. package/src/routes/_nav/settings/route.lazy.tsx +30 -0
  175. package/src/routes/_nav/settings/route.tsx +20 -0
  176. package/src/routes/_nav/sso/route.lazy.tsx +145 -0
  177. package/src/routes/_nav/sso/route.tsx +25 -0
  178. package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
  179. package/src/routes/_nav/users/$userId/route.tsx +27 -0
  180. package/src/routes/_nav/users/-components/users-table.tsx +38 -0
  181. package/src/routes/_nav/users/route.lazy.tsx +74 -0
  182. package/src/routes/_nav/users/route.tsx +43 -0
  183. package/src/routes/api/compliance-download/$jobId.ts +35 -0
  184. package/src/routes/api/observability/errors.ts +41 -0
  185. package/src/routes/api/observability/health.ts +30 -0
  186. package/src/routes/api.auth.$.ts +31 -0
  187. package/src/routes/api.sentry-test.ts +33 -0
  188. package/src/routes/index/route.lazy.tsx +22 -0
  189. package/src/routes/index/route.tsx +24 -0
  190. package/src/routes/login/route.lazy.tsx +46 -0
  191. package/src/routes/login/route.tsx +28 -0
  192. package/src/routes/sentry-tunnel.ts +61 -0
  193. package/src/routes/setup/route.lazy.tsx +94 -0
  194. package/src/routes/setup/route.tsx +28 -0
  195. package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
  196. package/src/server/__tests__/licenses-issue.test.ts +98 -0
  197. package/src/server/admin-ai-alerts.ts +177 -0
  198. package/src/server/admin-ai-usage.ts +134 -0
  199. package/src/server/auth-helpers.ts +51 -0
  200. package/src/server/entitlements.ts +24 -0
  201. package/src/server/knowledge-public/canonical-id.ts +55 -0
  202. package/src/server/knowledge-public/github-sync.ts +86 -0
  203. package/src/server/license-issue.ts +78 -0
  204. package/src/server/middlewares/request.ts +26 -0
  205. package/src/server/observability/debug-middleware.ts +39 -0
  206. package/src/server/observability/error-reporter.ts +99 -0
  207. package/src/server/observability/health-check.ts +140 -0
  208. package/src/server/observability/index.ts +33 -0
  209. package/src/server/observability/logger.ts +49 -0
  210. package/src/server/observability/metrics.ts +105 -0
  211. package/src/server/observability/request-middleware.ts +125 -0
  212. package/src/server/observability/timing.ts +42 -0
  213. package/src/server/pro-services.ts +32 -0
  214. package/src/server/serverFns/admin-billing.ts +94 -0
  215. package/src/server/serverFns/ai-provider/create.ts +92 -0
  216. package/src/server/serverFns/ai-provider/delete.ts +42 -0
  217. package/src/server/serverFns/ai-provider/get.ts +35 -0
  218. package/src/server/serverFns/ai-provider/list.ts +92 -0
  219. package/src/server/serverFns/ai-provider/update.ts +73 -0
  220. package/src/server/serverFns/anyhub/market.ts +64 -0
  221. package/src/server/serverFns/audit.ts +57 -0
  222. package/src/server/serverFns/auth/session.ts +26 -0
  223. package/src/server/serverFns/auth/setup.ts +73 -0
  224. package/src/server/serverFns/compliance.ts +85 -0
  225. package/src/server/serverFns/components/import.ts +197 -0
  226. package/src/server/serverFns/components/list.ts +51 -0
  227. package/src/server/serverFns/components/mutations.ts +84 -0
  228. package/src/server/serverFns/dependency/helpers.ts +245 -0
  229. package/src/server/serverFns/dependency/list.ts +127 -0
  230. package/src/server/serverFns/dependency/sync.ts +123 -0
  231. package/src/server/serverFns/dependency/update.ts +51 -0
  232. package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
  233. package/src/server/serverFns/developer-logs.ts +132 -0
  234. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
  235. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
  236. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
  237. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
  238. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
  239. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
  240. package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
  241. package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
  242. package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
  243. package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
  244. package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
  245. package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
  246. package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
  247. package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
  248. package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
  249. package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
  250. package/src/server/serverFns/licenses.ts +92 -0
  251. package/src/server/serverFns/locale.ts +10 -0
  252. package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
  253. package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
  254. package/src/server/serverFns/monitoring/get-errors.ts +43 -0
  255. package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
  256. package/src/server/serverFns/monitoring/get-health.ts +55 -0
  257. package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
  258. package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
  259. package/src/server/serverFns/monitoring/report-error.ts +55 -0
  260. package/src/server/serverFns/monitoring/reset.ts +70 -0
  261. package/src/server/serverFns/permissions.ts +214 -0
  262. package/src/server/serverFns/sentry/config.ts +88 -0
  263. package/src/server/serverFns/sso-config.ts +83 -0
  264. package/src/server/serverFns/user/create.ts +40 -0
  265. package/src/server/serverFns/user/delete.ts +29 -0
  266. package/src/server/serverFns/user/detail.ts +45 -0
  267. package/src/server/serverFns/user/list.ts +56 -0
  268. package/src/server/serverFns/user/reset-password.ts +28 -0
  269. package/src/server/serverFns/user/update-role.ts +26 -0
  270. package/src/server/serverFns/user/update-status.ts +26 -0
  271. package/src/server.ts +27 -0
  272. package/src/start.ts +85 -0
  273. package/src/styles/_keyframe-animations.scss +91 -0
  274. package/src/styles/_variables.scss +296 -0
  275. package/src/styles/globals.css +200 -0
  276. package/src/types/start-authjs.d.ts +27 -0
@@ -0,0 +1,86 @@
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
+ import { prisma } from '@allbluecn/database'
19
+
20
+ export interface GithubData {
21
+ stars: number
22
+ repoUpdatedAt: Date | null
23
+ latestVersion: string | null
24
+ versionDate: Date | null
25
+ }
26
+
27
+ const API = 'https://api.github.com'
28
+ const HEADERS: HeadersInit = {
29
+ Accept: 'application/vnd.github+json',
30
+ 'User-Agent': 'AllBlue-KnowledgeBase',
31
+ ...(process.env.GITHUB_TOKEN
32
+ ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
33
+ : {}),
34
+ }
35
+
36
+ export async function fetchGithubData(
37
+ owner: string,
38
+ repo: string,
39
+ ): Promise<GithubData | null> {
40
+ const repoRes = await fetch(`${API}/repos/${owner}/${repo}`, { headers: HEADERS })
41
+ if (!repoRes.ok) return null
42
+ const repoJson: any = await repoRes.json()
43
+
44
+ let latestVersion: string | null = null
45
+ let versionDate: Date | null = null
46
+ try {
47
+ const relRes = await fetch(`${API}/repos/${owner}/${repo}/releases/latest`, { headers: HEADERS })
48
+ if (relRes.ok) {
49
+ const relJson: any = await relRes.json()
50
+ latestVersion = relJson.tag_name ?? null
51
+ versionDate = relJson.published_at ? new Date(relJson.published_at) : null
52
+ }
53
+ } catch {
54
+ // ignore
55
+ }
56
+
57
+ return {
58
+ stars: repoJson.stargazers_count ?? 0,
59
+ repoUpdatedAt: repoJson.updated_at ? new Date(repoJson.updated_at) : null,
60
+ latestVersion,
61
+ versionDate,
62
+ }
63
+ }
64
+
65
+ export async function syncBookmarkGithub(bookmarkId: string): Promise<boolean> {
66
+ const bm = await prisma.websiteBookmark.findUnique({
67
+ where: { id: bookmarkId },
68
+ select: { id: true, isGithub: true, githubOwner: true, githubRepo: true },
69
+ })
70
+ if (!bm?.isGithub || !bm.githubOwner || !bm.githubRepo) return false
71
+
72
+ const data = await fetchGithubData(bm.githubOwner, bm.githubRepo)
73
+ await prisma.websiteBookmark.update({
74
+ where: { id: bookmarkId },
75
+ data: data
76
+ ? {
77
+ githubStars: data.stars,
78
+ githubRepoUpdatedAt: data.repoUpdatedAt,
79
+ githubLatestVersion: data.latestVersion,
80
+ githubVersionDate: data.versionDate,
81
+ githubSyncedAt: new Date(),
82
+ }
83
+ : { githubSyncedAt: new Date() },
84
+ })
85
+ return data !== null
86
+ }
@@ -0,0 +1,78 @@
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
+ import { prisma } from '@allbluecn/database'
19
+ import { getProService } from '@/server/pro-services'
20
+ import { FeatureRegistry } from '@allbluecn/kernel'
21
+ import { requireAdmin } from '@/server/auth-helpers'
22
+
23
+ export const issueLicense = async (data: {
24
+ customerName: string
25
+ customerEmail: string
26
+ customerId: string
27
+ plan: string
28
+ seats: number
29
+ features: Record<string, boolean>
30
+ expiresAt: string | null
31
+ }) => {
32
+ const session = await requireAdmin()
33
+
34
+ const unknownKeys = Object.keys(data.features).filter(
35
+ (k) => !(k in FeatureRegistry),
36
+ )
37
+ if (unknownKeys.length > 0) {
38
+ throw new Error(
39
+ `未注册的功能 key:${unknownKeys.join(', ')}(须先在 FeatureRegistry 注册)`,
40
+ )
41
+ }
42
+
43
+ const license = getProService('license')
44
+ if (!license?.issue) {
45
+ throw new Error(
46
+ '当前部署不支持 license 签发(需 Pro 签发环境 + LICENSE_SIGNING_PRIVATE_KEY)',
47
+ )
48
+ }
49
+
50
+ const expiresAt = data.expiresAt ? new Date(data.expiresAt) : null
51
+ const result = await license.issue({
52
+ customerName: data.customerName,
53
+ customerEmail: data.customerEmail,
54
+ customerId: data.customerId,
55
+ plan: data.plan,
56
+ seats: data.seats,
57
+ features: data.features,
58
+ expiresAt,
59
+ })
60
+
61
+ await prisma.licenseKey.create({
62
+ data: {
63
+ key: result.key,
64
+ signature: result.signature,
65
+ payload: result.payload as never,
66
+ customerId: data.customerId,
67
+ customerName: data.customerName,
68
+ customerEmail: data.customerEmail,
69
+ plan: result.payload.plan,
70
+ seats: data.seats,
71
+ features: data.features as never,
72
+ expiresAt,
73
+ issuedById: session.user!.id,
74
+ },
75
+ })
76
+
77
+ return { key: result.key }
78
+ }
@@ -0,0 +1,26 @@
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
+ import { createMiddleware } from '@tanstack/react-start'
19
+
20
+ export interface RequestContext {
21
+ request: Request
22
+ }
23
+
24
+ export const requestContextMiddleware = createMiddleware().server<RequestContext>(async ({ request, next }) => {
25
+ return next({ context: { request } })
26
+ })
@@ -0,0 +1,39 @@
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
+ import { createMiddleware } from '@tanstack/react-start'
19
+
20
+ export const debugMiddleware = createMiddleware().server(async ({ next }: any) => {
21
+ const result = await next() as any
22
+
23
+ if (process.env.NODE_ENV === 'development') {
24
+ const response = result.response || result
25
+ if (response instanceof Response) {
26
+ const headers = new Headers(response.headers)
27
+ headers.set('X-Debug-Timestamp', new Date().toISOString())
28
+ headers.set('X-Debug-Node-Version', process.version)
29
+ headers.set('X-Debug-Uptime', process.uptime().toString())
30
+ return new Response(response.body, {
31
+ status: response.status,
32
+ statusText: response.statusText,
33
+ headers,
34
+ })
35
+ }
36
+ }
37
+
38
+ return result
39
+ })
@@ -0,0 +1,99 @@
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
+ import { Prisma, prisma } from '@allbluecn/database'
19
+
20
+ interface ErrorRecord {
21
+ name: string
22
+ message: string
23
+ stack?: string
24
+ context?: Record<string, unknown>
25
+ }
26
+
27
+ interface ErrorEntry {
28
+ count: number
29
+ lastSeen: Date
30
+ error: ErrorRecord
31
+ }
32
+
33
+ const errorStore = new Map<string, ErrorEntry>()
34
+ let initialized = false
35
+
36
+ export function reportError(error: Error, context?: Record<string, unknown>): void {
37
+ const key = `${error.name}:${error.message}`
38
+ const existing = errorStore.get(key)
39
+
40
+ if (existing) {
41
+ existing.count++
42
+ existing.lastSeen = new Date()
43
+ } else {
44
+ errorStore.set(key, {
45
+ count: 1,
46
+ lastSeen: new Date(),
47
+ error: { name: error.name, message: error.message, stack: error.stack, context },
48
+ })
49
+ }
50
+
51
+ const entry = errorStore.get(key)!
52
+ saveErrorReportAsync(key, entry).catch((e) => console.error('[ErrorReportSave]', e))
53
+ }
54
+
55
+ async function saveErrorReportAsync(key: string, entry: ErrorEntry): Promise<void> {
56
+ await prisma.monitoringErrorReport.upsert({
57
+ where: { id: key },
58
+ create: {
59
+ id: key,
60
+ errorName: entry.error.name,
61
+ errorMessage: entry.error.message,
62
+ stackTrace: entry.error.stack,
63
+ count: entry.count,
64
+ firstSeen: entry.lastSeen,
65
+ lastSeen: entry.lastSeen,
66
+ context: (entry.error.context as Prisma.InputJsonValue) ?? null,
67
+ },
68
+ update: {
69
+ count: entry.count,
70
+ lastSeen: entry.lastSeen,
71
+ },
72
+ })
73
+ }
74
+
75
+ export function getErrorStore(): Array<{ key: string; entry: ErrorEntry }> {
76
+ return Array.from(errorStore.entries()).map(([key, entry]) => ({ key, entry }))
77
+ }
78
+
79
+ export function resetErrors(): void {
80
+ errorStore.clear()
81
+ }
82
+
83
+ export function initErrorCollector(): void {
84
+ if (initialized) return
85
+ initialized = true
86
+
87
+ process.on('unhandledRejection', (reason: unknown) => {
88
+ const error = reason instanceof Error
89
+ ? reason
90
+ : new Error(`Unhandled rejection: ${String(reason)}`)
91
+ reportError(error, { source: 'unhandledRejection' })
92
+ })
93
+
94
+ process.on('uncaughtException', (error, origin) => {
95
+ reportError(error, { source: 'uncaughtException', origin })
96
+ console.error('[FATAL]', error.message || 'Uncaught exception')
97
+ process.exit(1)
98
+ })
99
+ }
@@ -0,0 +1,140 @@
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
+ import os from 'node:os'
19
+ import { prisma } from '@allbluecn/database'
20
+
21
+ interface HealthResult {
22
+ status: 'healthy' | 'unhealthy'
23
+ checks: {
24
+ uptime: number
25
+ memory: NodeJS.MemoryUsage
26
+ database: { status: 'connected' | 'error'; latency?: number; error?: string }
27
+ cpu: { usage: number; cores: number; model: string }
28
+ disk: { total: number; free: number; usedPct: number }
29
+ load: { one: number; five: number; fifteen: number }
30
+ system: { platform: string; type: string; release: string; nodeVersion: string }
31
+ }
32
+ }
33
+
34
+ let healthTimer: ReturnType<typeof setInterval> | null = null
35
+ let healthEnabled = true
36
+
37
+ export async function performHealthCheck(): Promise<HealthResult> {
38
+ let dbResult: { status: 'connected' | 'error'; latency?: number; error?: string }
39
+
40
+ try {
41
+ const dbStart = Date.now()
42
+ await prisma.$queryRaw`SELECT 1`
43
+ const dbLatency = Date.now() - dbStart
44
+ dbResult = { status: 'connected', latency: dbLatency }
45
+ } catch (e) {
46
+ dbResult = { status: 'error', error: (e as Error).message }
47
+ }
48
+
49
+ const status: HealthResult['status'] = dbResult.status === 'error' ? 'unhealthy' : 'healthy'
50
+ const memory = process.memoryUsage()
51
+ const cpus = os.cpus()
52
+ const cpuCount = cpus.length
53
+ const cpuModel = cpus[0]?.model || 'unknown'
54
+ const loadavg = os.loadavg()
55
+ const totalMem = os.totalmem()
56
+ const freeMem = os.freemem()
57
+ const usedPct = ((totalMem - freeMem) / totalMem) * 100
58
+
59
+ const snapshot = {
60
+ status,
61
+ uptimeSeconds: process.uptime(),
62
+ memoryUsage: {
63
+ rss: memory.rss,
64
+ heapTotal: memory.heapTotal,
65
+ heapUsed: memory.heapUsed,
66
+ external: memory.external,
67
+ arrayBuffers: memory.arrayBuffers,
68
+ },
69
+ dbStatus: dbResult.status,
70
+ dbLatencyMs: dbResult.latency || null,
71
+ }
72
+
73
+ await prisma.monitoringHealthSnapshot.create({ data: snapshot })
74
+
75
+ return {
76
+ status,
77
+ checks: {
78
+ uptime: process.uptime(),
79
+ memory,
80
+ database: dbResult,
81
+ cpu: { usage: usedPct, cores: cpuCount, model: cpuModel },
82
+ disk: { total: totalMem, free: freeMem, usedPct },
83
+ load: { one: loadavg[0], five: loadavg[1], fifteen: loadavg[2] },
84
+ system: {
85
+ platform: os.platform(),
86
+ type: os.type(),
87
+ release: os.release(),
88
+ nodeVersion: process.version,
89
+ },
90
+ },
91
+ }
92
+ }
93
+
94
+ export function startHealthCheckTimer(intervalMs = 30_000): void {
95
+ if (healthTimer) clearInterval(healthTimer)
96
+ performHealthCheck().catch((e) => console.error('[HealthCheck]', e))
97
+ healthTimer = setInterval(() => {
98
+ performHealthCheck().catch((e) => console.error('[HealthCheck]', e))
99
+ }, intervalMs)
100
+ healthEnabled = true
101
+ }
102
+
103
+ export function stopHealthCheckTimer(): void {
104
+ if (healthTimer) {
105
+ clearInterval(healthTimer)
106
+ healthTimer = null
107
+ }
108
+ healthEnabled = false
109
+ }
110
+
111
+ export function isHealthCheckEnabled(): boolean {
112
+ return healthEnabled
113
+ }
114
+
115
+ export async function setHealthCheckEnabled(enabled: boolean): Promise<void> {
116
+ if (enabled) {
117
+ startHealthCheckTimer()
118
+ } else {
119
+ stopHealthCheckTimer()
120
+ }
121
+
122
+ await prisma.systemSetting.upsert({
123
+ where: { key: 'monitoring_health_check_enabled' },
124
+ create: {
125
+ key: 'monitoring_health_check_enabled',
126
+ value: enabled ? 'true' : 'false',
127
+ category: 'monitoring',
128
+ desc: '监控健康检查自动定时开关',
129
+ },
130
+ update: { value: enabled ? 'true' : 'false' },
131
+ })
132
+ }
133
+
134
+ export async function getHealthCheckEnabled(): Promise<boolean> {
135
+ const setting = await prisma.systemSetting.findUnique({
136
+ where: { key: 'monitoring_health_check_enabled' },
137
+ })
138
+ if (!setting) return true
139
+ return setting.value === 'true'
140
+ }
@@ -0,0 +1,33 @@
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
+ export { logger } from './logger'
19
+ export { metrics } from './metrics'
20
+ export { reportError, getErrorStore, resetErrors, initErrorCollector } from './error-reporter'
21
+ export {
22
+ performHealthCheck,
23
+ startHealthCheckTimer,
24
+ stopHealthCheckTimer,
25
+ isHealthCheckEnabled,
26
+ setHealthCheckEnabled,
27
+ getHealthCheckEnabled,
28
+ } from './health-check'
29
+ export { observabilityMiddleware, requestLogStore } from './request-middleware'
30
+ export { debugMiddleware } from './debug-middleware'
31
+ export { timed } from './timing'
32
+ export type { LogLevel } from './logger'
33
+ export type { TimingResult } from './timing'
@@ -0,0 +1,49 @@
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
+ import { createIsomorphicFn } from '@tanstack/react-start'
19
+
20
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
21
+
22
+ type LogData = Record<string, unknown>
23
+
24
+ const logger = createIsomorphicFn()
25
+ .server((level: LogLevel, message: string, data?: LogData) => {
26
+ const timestamp = new Date().toISOString()
27
+
28
+ if (process.env.NODE_ENV === 'development') {
29
+ (console as any)[level](`[${timestamp}] [${level.toUpperCase()}]`, message, data)
30
+ } else {
31
+ console.log(
32
+ JSON.stringify({
33
+ timestamp,
34
+ level,
35
+ message,
36
+ data,
37
+ service: 'allblue-admin',
38
+ environment: process.env.NODE_ENV,
39
+ }),
40
+ )
41
+ }
42
+ })
43
+ .client((level: LogLevel, message: string, data?: LogData) => {
44
+ if (process.env.NODE_ENV === 'development') {
45
+ (console as any)[level](`[CLIENT] [${level.toUpperCase()}]`, message, data)
46
+ }
47
+ })
48
+
49
+ export { logger }
@@ -0,0 +1,105 @@
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
+ import { prisma } from '@allbluecn/database'
19
+
20
+ interface MetricStats {
21
+ count: number
22
+ avg: number
23
+ p50: number
24
+ p95: number
25
+ p99: number
26
+ min: number
27
+ max: number
28
+ }
29
+
30
+ class MetricsCollector {
31
+ private metrics = new Map<string, number[]>()
32
+ private readonly MAX_SAMPLES = 1000
33
+ private flushTimer?: ReturnType<typeof setInterval>
34
+
35
+ constructor() {
36
+ this.flushTimer = setInterval(() => {
37
+ this.flushToDb().catch((e) => console.error('[MetricsFlush]', e))
38
+ }, 60_000)
39
+ }
40
+
41
+ recordTiming(name: string, duration: number): void {
42
+ if (!this.metrics.has(name)) {
43
+ this.metrics.set(name, [])
44
+ }
45
+ const samples = this.metrics.get(name)!
46
+ samples.push(duration)
47
+ if (samples.length > this.MAX_SAMPLES) {
48
+ samples.shift()
49
+ }
50
+ }
51
+
52
+ getStats(name: string): MetricStats | null {
53
+ const timings = this.metrics.get(name)
54
+ if (!timings || timings.length === 0) return null
55
+
56
+ const sorted = [...timings].sort((a, b) => a - b)
57
+ const sum = sorted.reduce((a, b) => a + b, 0)
58
+ return {
59
+ count: sorted.length,
60
+ avg: Math.round(sum / sorted.length * 100) / 100,
61
+ p50: sorted[Math.floor(sorted.length * 0.5)],
62
+ p95: sorted[Math.floor(sorted.length * 0.95)],
63
+ p99: sorted[Math.floor(sorted.length * 0.99)],
64
+ min: sorted[0],
65
+ max: sorted[sorted.length - 1],
66
+ }
67
+ }
68
+
69
+ getAllStats(): Record<string, MetricStats> {
70
+ const stats: Record<string, MetricStats> = {}
71
+ for (const [name, timings] of this.metrics) {
72
+ const s = this.getStats(name)
73
+ if (s) stats[name] = s
74
+ }
75
+ return stats
76
+ }
77
+
78
+ async flushToDb(): Promise<void> {
79
+ 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
+ }
94
+ }
95
+
96
+ reset(): void {
97
+ this.metrics.clear()
98
+ }
99
+
100
+ destroy(): void {
101
+ if (this.flushTimer) clearInterval(this.flushTimer)
102
+ }
103
+ }
104
+
105
+ export const metrics = new MetricsCollector()