@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,87 @@
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
+ // apps/admin/src/components/ai-provider/provider-logo.tsx
19
+ import { useEffect, useState } from "react"
20
+ import type { AiProviderMeta } from "@allbluecn/kernel"
21
+ import { LOBE_ICON_MAP } from "@allbluecn/kernel"
22
+
23
+ type LobeIconComponent = React.ComponentType<{ size?: number; className?: string }>
24
+
25
+ async function loadLobeIcon(lobeName: string): Promise<LobeIconComponent | null> {
26
+ try {
27
+ const mod = await import("@lobehub/icons")
28
+ return (mod as unknown as Record<string, LobeIconComponent>)[lobeName] ?? null
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+
34
+ function InitialFallback({
35
+ brandName,
36
+ size,
37
+ }: {
38
+ brandName: string
39
+ size: number
40
+ }) {
41
+ return (
42
+ <span
43
+ style={{
44
+ width: size,
45
+ height: size,
46
+ display: "inline-flex",
47
+ alignItems: "center",
48
+ justifyContent: "center",
49
+ fontSize: size * 0.5,
50
+ fontWeight: 700,
51
+ color: "currentColor",
52
+ }}
53
+ >
54
+ {brandName.charAt(0).toUpperCase()}
55
+ </span>
56
+ )
57
+ }
58
+
59
+ export function ProviderLogo({
60
+ provider,
61
+ size = 24,
62
+ }: {
63
+ provider: Pick<AiProviderMeta, "providerId" | "brandName">
64
+ size?: number
65
+ }) {
66
+ const [IconComponent, setIconComponent] = useState<LobeIconComponent | null>(null)
67
+
68
+ useEffect(() => {
69
+ const lobeName = LOBE_ICON_MAP[provider.providerId]
70
+ if (!lobeName) {
71
+ setIconComponent(null)
72
+ return
73
+ }
74
+ let cancelled = false
75
+ loadLobeIcon(lobeName).then((icon) => {
76
+ if (!cancelled) setIconComponent(icon)
77
+ })
78
+ return () => { cancelled = true }
79
+ }, [provider.providerId])
80
+
81
+ // admin 使用单色版本(默认导出即 mono)
82
+ if (IconComponent) {
83
+ return <IconComponent size={size} />
84
+ }
85
+
86
+ return <InitialFallback brandName={provider.brandName} size={size} />
87
+ }
@@ -0,0 +1,258 @@
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
+ // apps/admin/src/components/ai-provider/provider-table.tsx
19
+ import { useState } from "react"
20
+ import { useTranslation } from 'react-i18next'
21
+ import { useMutation } from "@tanstack/react-query"
22
+ import { Link, useRouter } from "@tanstack/react-router"
23
+ import { Edit, Plus, Trash2, Users } from "lucide-react"
24
+ import { toast } from '@/components/ui/sonner'
25
+ import { ADAPTER_KINDS, PROVIDER_BADGE_STYLES, type AiProviderRow } from "@allbluecn/kernel"
26
+ import { Button } from "@/components/ui/button"
27
+ import { Switch } from "@/components/ui/switch"
28
+ import { Badge } from "@/components/ui/badge"
29
+ import {
30
+ AlertDialog,
31
+ AlertDialogAction,
32
+ AlertDialogCancel,
33
+ AlertDialogContent,
34
+ AlertDialogDescription,
35
+ AlertDialogFooter,
36
+ AlertDialogHeader,
37
+ AlertDialogTitle,
38
+ } from "@/components/ui/alert-dialog"
39
+ import { DataTable, type AdminColumnDef } from "@/components/data-table/data-table"
40
+ import { ProviderIcon } from "./provider-icon"
41
+ import { updateAiProviderAdminFn } from "@/server/serverFns/ai-provider/update"
42
+ import { deleteAiProviderAdminFn } from "@/server/serverFns/ai-provider/delete"
43
+
44
+ const ADAPTER_LABEL_MAP = new Map(ADAPTER_KINDS.map((k) => [k.value, k.label]))
45
+
46
+ export function ProviderTable({ data }: { data: AiProviderRow[] }) {
47
+ const { t } = useTranslation('admin-ai')
48
+ const router = useRouter()
49
+ const [pendingDelete, setPendingDelete] = useState<AiProviderRow | null>(null)
50
+
51
+ const toggleMutation = useMutation({
52
+ mutationFn: (vars: { id: string; enabled: boolean }) =>
53
+ updateAiProviderAdminFn({
54
+ data: { id: vars.id, enabled: vars.enabled },
55
+ }),
56
+ onSuccess: async () => {
57
+ toast.success(t('table.toast.updated'))
58
+ await router.invalidate()
59
+ },
60
+ onError: (err) =>
61
+ toast.error(err instanceof Error ? err.message : t('market.form.operationFailed')),
62
+ })
63
+
64
+ const deleteMutation = useMutation({
65
+ mutationFn: (id: string) => deleteAiProviderAdminFn({ data: { id } }),
66
+ onSuccess: async () => {
67
+ toast.success(t('table.toast.deleted'))
68
+ setPendingDelete(null)
69
+ await router.invalidate()
70
+ },
71
+ onError: (err) =>
72
+ toast.error(err instanceof Error ? err.message : t('table.toast.deleteFailed')),
73
+ })
74
+
75
+ const columns: AdminColumnDef<AiProviderRow>[] = [
76
+ {
77
+ accessorKey: "displayName",
78
+ header: t('table.columns.provider'),
79
+ cell: ({ row }) => (
80
+ <div className="flex items-center gap-3">
81
+ <ProviderIcon provider={row.original} size={36} />
82
+ <div>
83
+ <div className="font-medium">{row.original.displayName}</div>
84
+ <div className="font-mono text-xs text-muted-foreground">
85
+ {row.original.providerId}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ ),
90
+ },
91
+ {
92
+ accessorKey: "adapterKind",
93
+ header: t('table.columns.adapterKind'),
94
+ cell: ({ row }) => (
95
+ <Badge variant="outline">
96
+ {ADAPTER_LABEL_MAP.get(row.original.adapterKind) ??
97
+ row.original.adapterKind}
98
+ </Badge>
99
+ ),
100
+ },
101
+ {
102
+ accessorKey: "category",
103
+ header: t('table.columns.category'),
104
+ cell: ({ row }) => (
105
+ <Badge variant="secondary">{row.original.category}</Badge>
106
+ ),
107
+ },
108
+ {
109
+ accessorKey: "badge",
110
+ header: t('table.columns.badge'),
111
+ cell: ({ row }) => {
112
+ const b = row.original.badge
113
+ if (!b) return <span className="text-muted-foreground">—</span>
114
+ const style = PROVIDER_BADGE_STYLES[b] ?? "bg-muted text-muted-foreground"
115
+ return (
116
+ <span className={`inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium ${style}`}>
117
+ {b}
118
+ </span>
119
+ )
120
+ },
121
+ },
122
+ {
123
+ accessorKey: "models",
124
+ header: t('table.columns.modelCount'),
125
+ cell: ({ row }) => (
126
+ <span className="text-sm">
127
+ {Array.isArray(row.original.models) ? row.original.models.length : 0}
128
+ </span>
129
+ ),
130
+ },
131
+ {
132
+ accessorKey: "configCount",
133
+ header: () => (
134
+ <span className="inline-flex items-center gap-1">
135
+ <Users className="size-3.5" />
136
+ {t('table.columns.userConfigCount')}
137
+ </span>
138
+ ),
139
+ cell: ({ row }) => (
140
+ <span
141
+ className={
142
+ row.original.configCount > 0
143
+ ? "font-medium"
144
+ : "text-muted-foreground"
145
+ }
146
+ >
147
+ {row.original.configCount}
148
+ </span>
149
+ ),
150
+ },
151
+ {
152
+ accessorKey: "enabled",
153
+ header: t('table.columns.platformEnabled'),
154
+ cell: ({ row }) => (
155
+ <Switch
156
+ checked={row.original.enabled}
157
+ onCheckedChange={(checked) =>
158
+ toggleMutation.mutate({
159
+ id: row.original.id,
160
+ enabled: checked,
161
+ })
162
+ }
163
+ disabled={toggleMutation.isPending}
164
+ aria-label={t('table.enabledToggle')}
165
+ />
166
+ ),
167
+ },
168
+ {
169
+ id: "actions",
170
+ header: t('table.columns.actions'),
171
+ cell: ({ row }) => {
172
+ const canDelete = row.original.configCount === 0
173
+ return (
174
+ <div className="flex items-center gap-1">
175
+ <Button asChild variant="ghost" size="sm">
176
+ <Link
177
+ to="/ai-providers/$id/edit"
178
+ params={{ id: row.original.id }}
179
+ >
180
+ <Edit className="size-4" />
181
+ {t('table.edit')}
182
+ </Link>
183
+ </Button>
184
+ {canDelete ? (
185
+ <Button
186
+ variant="ghost"
187
+ size="sm"
188
+ className="text-destructive hover:bg-destructive/10 hover:text-destructive"
189
+ onClick={() => setPendingDelete(row.original)}
190
+ >
191
+ <Trash2 className="size-4" />
192
+ {t('table.delete')}
193
+ </Button>
194
+ ) : (
195
+ <Button
196
+ variant="ghost"
197
+ size="sm"
198
+ disabled
199
+ title={t('table.deleteDisabled', { count: row.original.configCount })}
200
+ >
201
+ <Trash2 className="size-4" />
202
+ {t('table.delete')}
203
+ </Button>
204
+ )}
205
+ </div>
206
+ )
207
+ },
208
+ },
209
+ ]
210
+
211
+ return (
212
+ <div className="space-y-4">
213
+ <div className="flex items-center justify-between">
214
+ <div>
215
+ <h2 className="text-lg font-semibold">{t('table.pageTitle')}</h2>
216
+ <p className="text-sm text-muted-foreground">
217
+ {t('table.pageDescription', { count: data.length })}
218
+ </p>
219
+ </div>
220
+ <Button asChild>
221
+ <Link to="/ai-providers/new">
222
+ <Plus className="size-4" />
223
+ {t('table.addProvider')}
224
+ </Link>
225
+ </Button>
226
+ </div>
227
+ <DataTable columns={columns} data={data} />
228
+
229
+ <AlertDialog
230
+ open={!!pendingDelete}
231
+ onOpenChange={(open) => !open && setPendingDelete(null)}
232
+ >
233
+ <AlertDialogContent>
234
+ <AlertDialogHeader>
235
+ <AlertDialogTitle>
236
+ {t('table.deleteConfirm.title', { name: pendingDelete?.displayName })}
237
+ </AlertDialogTitle>
238
+ <AlertDialogDescription>
239
+ {t('table.deleteConfirm.description')}
240
+ </AlertDialogDescription>
241
+ </AlertDialogHeader>
242
+ <AlertDialogFooter>
243
+ <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
244
+ <AlertDialogAction
245
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
246
+ onClick={() =>
247
+ pendingDelete && deleteMutation.mutate(pendingDelete.id)
248
+ }
249
+ disabled={deleteMutation.isPending}
250
+ >
251
+ {t('table.deleteConfirm.confirm')}
252
+ </AlertDialogAction>
253
+ </AlertDialogFooter>
254
+ </AlertDialogContent>
255
+ </AlertDialog>
256
+ </div>
257
+ )
258
+ }
@@ -0,0 +1,214 @@
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 { LogOut, User, Mail, Users, BarChart3, Shield, Settings, ArrowRight, ArrowLeft, Target } from "lucide-react";
19
+ import { useState } from "react";
20
+ import { useTranslation } from 'react-i18next'
21
+ import { signIn, signOut } from "@/lib/auth-client";
22
+ import { Button } from "@/components/ui/button";
23
+ import { Checkbox } from "@/components/ui/checkbox";
24
+ import { Field, FieldLabel } from "@/components/ui/field";
25
+ import { Card, CardContent } from "@/components/ui/card";
26
+ import { toast } from '@/components/ui/sonner';
27
+ import { UserAvatar } from "@allbluecn/web-app/user-avatar";
28
+ import type { AvatarConfig, ShapeType } from "@allbluecn/kernel";
29
+ import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from "@/components/auth/auth-layout";
30
+ import { AuthHeader } from "@/components/auth/auth-header";
31
+ import { AuthBrandPanel } from "@/components/auth/auth-brand-panel";
32
+ import { IconInput } from "@/components/auth/icon-input";
33
+ import { PasswordInput } from "@/components/auth/password-input";
34
+ import { RainbowButton } from "@/components/magicui/rainbow-button";
35
+
36
+ interface AdminLoginFormProps {
37
+ user?: {
38
+ name?: string | null;
39
+ email?: string | null;
40
+ avatarConfig?: AvatarConfig;
41
+ bgShape?: string;
42
+ bgColor?: string;
43
+ } | null;
44
+ isAdmin: boolean;
45
+ }
46
+
47
+ const brandPanel = (
48
+ <AuthBrandPanel title="ProtoX 管理后台" subtitle="高效管理您的产品工作台" features={[
49
+ { icon: Users, label: "团队管理", desc: "成员与角色" },
50
+ { icon: BarChart3, label: "数据看板", desc: "系统概览" },
51
+ { icon: Shield, label: "权限控制", desc: "安全策略" },
52
+ { icon: Settings, label: "系统设置", desc: "配置管理" },
53
+ ]} />
54
+ );
55
+
56
+ function maskEmail(email: string): string {
57
+ const [local, domain] = email.split("@");
58
+ if (!domain) return email;
59
+ const prefix = local.length > 3 ? local.slice(0, 3) : local;
60
+ return `${prefix}***@${domain}`;
61
+ }
62
+
63
+ export default function AdminLoginForm({ user, isAdmin }: AdminLoginFormProps) {
64
+ const { t } = useTranslation('common')
65
+ const [password, setPassword] = useState("");
66
+ const [email, setEmail] = useState("");
67
+ const [stayLoggedIn, setStayLoggedIn] = useState(false);
68
+ const [loading, setLoading] = useState(false);
69
+
70
+ async function handleLogin(e: React.FormEvent) {
71
+ e.preventDefault();
72
+
73
+ if (!isAdmin && !email) {
74
+ toast.error(t('login.error.enterEmail'));
75
+ return;
76
+ }
77
+ if (!isAdmin && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) {
78
+ toast.error(t('login.error.validEmail'));
79
+ return;
80
+ }
81
+ if (!password) {
82
+ toast.error(t('login.error.enterPassword'));
83
+ return;
84
+ }
85
+
86
+ setLoading(true);
87
+
88
+ try {
89
+ // signIn 内部用 form.submit() 触发浏览器原生 POST + 302 全页刷新,
90
+ // 浏览器自动处理 Set-Cookie 和重定向到 callbackUrl。
91
+ // 禁止在 signIn 后调用 router.navigate——router context 中 session 仍是旧值(空),
92
+ // _nav beforeLoad 会因 session 校验失败重定向回 /login(反模式,见 memrec 885bdd10)。
93
+ await signIn("credentials", {
94
+ redirectTo: "/dashboard",
95
+ email: isAdmin ? (user?.email || email) : email,
96
+ password,
97
+ remember: stayLoggedIn,
98
+ });
99
+ // form.submit() 已触发导航,以下代码在页面卸载前执行
100
+ // 若 csrf fetch 失败会抛错进入 catch
101
+ } catch {
102
+ toast.error(t('login.error.invalidCredentials'));
103
+ setLoading(false);
104
+ }
105
+ }
106
+
107
+ async function handleLogout() {
108
+ await signOut();
109
+ window.location.reload();
110
+ }
111
+
112
+ async function handleSwitchAccount() {
113
+ await signOut();
114
+ window.location.reload();
115
+ }
116
+
117
+ if (isAdmin && user?.email) {
118
+ return (
119
+ <AuthLayout>
120
+ <AuthLayoutLeft>
121
+ <div className="space-y-4 text-center">
122
+ <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-lg bg-primary">
123
+ <Target className="h-10 w-10 text-primary-foreground" />
124
+ </div>
125
+ <h1 className="text-3xl font-semibold">{t('login.adminPanel')}</h1>
126
+ <Card className="h-16 w-full bg-muted/50 border">
127
+ <CardContent className="flex h-full items-center gap-3 px-3 py-3">
128
+ <UserAvatar
129
+ avatarConfig={user?.avatarConfig}
130
+ bgShape={user?.bgShape as ShapeType | undefined}
131
+ bgColor={user?.bgColor}
132
+ name={user?.name || user?.email || ""}
133
+ size="lg"
134
+ />
135
+ <div className="min-w-0 text-left">
136
+ <p className="truncate text-base font-medium">{user?.name || user?.email || t('login.admin')}</p>
137
+ <p className="truncate text-xm text-muted-foreground">{maskEmail(user?.email || "")}</p>
138
+ </div>
139
+ </CardContent>
140
+ </Card>
141
+ </div>
142
+ <form onSubmit={handleLogin} className="space-y-6">
143
+ <PasswordInput label={t('login.password.label')} placeholder={t('login.password.placeholder')} value={password} onChange={(e) => setPassword(e.target.value)} />
144
+ <Field orientation="horizontal" className="gap-2">
145
+ <FieldLabel>
146
+ <Checkbox checked={stayLoggedIn} onCheckedChange={(v) => setStayLoggedIn(v === true)} />
147
+ <span className="text-sm">{t('login.rememberMe')}</span>
148
+ </FieldLabel>
149
+ </Field>
150
+ <RainbowButton type="submit" className="w-full" disabled={loading}>
151
+ {loading ? t('login.loading') : t('login.loginAdmin')}
152
+ <ArrowRight className="ml-2 h-4 w-4" />
153
+ </RainbowButton>
154
+ <div className="flex gap-2">
155
+ <Button type="button" variant="outline" className="flex-1" onClick={handleSwitchAccount}>
156
+ <Users className="w-4 h-4 mr-2" />{t('login.switchAccount')}
157
+ </Button>
158
+ <Button type="button" variant="outline" className="flex-1" onClick={handleLogout}>
159
+ <LogOut className="w-4 h-4 mr-2" />{t('login.logout')}
160
+ </Button>
161
+ </div>
162
+ </form>
163
+ </AuthLayoutLeft>
164
+ <AuthLayoutRight>{brandPanel}</AuthLayoutRight>
165
+ </AuthLayout>
166
+ );
167
+ }
168
+
169
+ if (user && !isAdmin) {
170
+ return (
171
+ <AuthLayout>
172
+ <AuthLayoutLeft>
173
+ <div className="space-y-4 text-center">
174
+ <AuthHeader title={t('login.noPermission')} titleClassName="text-destructive" />
175
+ <p className="text-muted-foreground">
176
+ {t('login.notAdmin', { name: user.name || user.email })}
177
+ </p>
178
+ <Button className="w-full" onClick={() => window.open(`${import.meta.env.VITE_WEB_URL ?? '/'}dashboard`, "_self")}>
179
+ <ArrowLeft className="h-4 w-4" />
180
+ {t('login.backToWorkspace')}
181
+ </Button>
182
+ <Button variant="outline" className="w-full" onClick={handleSwitchAccount}>
183
+ {t('login.switchAndLogin')}
184
+ </Button>
185
+ </div>
186
+ </AuthLayoutLeft>
187
+ <AuthLayoutRight>{brandPanel}</AuthLayoutRight>
188
+ </AuthLayout>
189
+ );
190
+ }
191
+
192
+ return (
193
+ <AuthLayout>
194
+ <AuthLayoutLeft>
195
+ <AuthHeader title={t('login.adminPanel')} subtitle={t('login.subtitle')} />
196
+ <form onSubmit={handleLogin} className="space-y-6">
197
+ <IconInput icon={Mail} label={t('login.email.label')} placeholder={t('login.email.placeholder')} value={email} onChange={(e) => setEmail(e.target.value)} />
198
+ <PasswordInput label={t('login.password.label')} placeholder={t('login.password.placeholder')} value={password} onChange={(e) => setPassword(e.target.value)} />
199
+ <Field orientation="horizontal" className="gap-2">
200
+ <FieldLabel>
201
+ <Checkbox checked={stayLoggedIn} onCheckedChange={(v) => setStayLoggedIn(v === true)} />
202
+ <span className="text-sm">{t('login.rememberMe')}</span>
203
+ </FieldLabel>
204
+ </Field>
205
+ <RainbowButton type="submit" className="w-full" disabled={loading}>
206
+ {loading ? t('login.loggingIn') : t('login.loginAdmin')}
207
+ <ArrowRight className="ml-2 h-4 w-4" />
208
+ </RainbowButton>
209
+ </form>
210
+ </AuthLayoutLeft>
211
+ <AuthLayoutRight>{brandPanel}</AuthLayoutRight>
212
+ </AuthLayout>
213
+ );
214
+ }
@@ -0,0 +1,60 @@
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 type { LucideIcon } from 'lucide-react'
19
+ import type { ReactNode } from 'react'
20
+
21
+ interface BrandFeature {
22
+ icon: LucideIcon
23
+ label: string
24
+ desc: string
25
+ }
26
+
27
+ export type { BrandFeature }
28
+
29
+ interface AuthBrandPanelProps {
30
+ title: string
31
+ subtitle?: string
32
+ features?: BrandFeature[]
33
+ footer?: ReactNode
34
+ }
35
+
36
+ export function AuthBrandPanel({ title, subtitle, features, footer }: AuthBrandPanelProps) {
37
+ return (
38
+ <>
39
+ <h1 className="text-4xl font-bold tracking-tight">{title}</h1>
40
+ {subtitle && (
41
+ <p className="text-lg text-muted-foreground">{subtitle}</p>
42
+ )}
43
+ {features && features.length > 0 && (
44
+ <div className="grid grid-cols-2 gap-4 pt-8">
45
+ {features.map((feature) => {
46
+ const Icon = feature.icon
47
+ return (
48
+ <div key={feature.label} className="text-center p-4 rounded-lg bg-background/50">
49
+ <Icon className="h-8 w-8 mx-auto mb-2 text-primary" />
50
+ <p className="font-semibold">{feature.label}</p>
51
+ <p className="text-xs text-muted-foreground">{feature.desc}</p>
52
+ </div>
53
+ )
54
+ })}
55
+ </div>
56
+ )}
57
+ {footer && <div className="pt-4">{footer}</div>}
58
+ </>
59
+ )
60
+ }
@@ -0,0 +1,38 @@
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 { Target } from 'lucide-react'
19
+
20
+ interface AuthHeaderProps {
21
+ title: string
22
+ subtitle?: string
23
+ titleClassName?: string
24
+ }
25
+
26
+ export function AuthHeader({ title, subtitle, titleClassName }: AuthHeaderProps) {
27
+ return (
28
+ <div className="space-y-2 text-center">
29
+ <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-lg bg-primary">
30
+ <Target className="h-10 w-10 text-primary-foreground" />
31
+ </div>
32
+ <h1 className={`text-3xl font-semibold ${titleClassName || ''}`}>{title}</h1>
33
+ {subtitle && (
34
+ <p className="text-muted-foreground">{subtitle}</p>
35
+ )}
36
+ </div>
37
+ )
38
+ }