@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,264 @@
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 { StartAuthJSConfig } from 'start-authjs'
19
+ import type { SessionUser } from '@/lib/session'
20
+ import CredentialsProvider from '@auth/core/providers/credentials'
21
+ import type { PrismaClient } from '@allbluecn/database'
22
+ import bcrypt from 'bcryptjs'
23
+ import { getRandomConfig, presetBgColors } from '@/components/settings/avatar/avatar-config'
24
+ import { resolveIpLocation, getClientIp } from '@allbluecn/kernel'
25
+
26
+ let _prisma: PrismaClient | null = null
27
+ async function getDb(): Promise<PrismaClient> {
28
+ if (!_prisma) {
29
+ const mod = await import('@allbluecn/database')
30
+ _prisma = mod.prisma
31
+ }
32
+ return _prisma
33
+ }
34
+
35
+ function pickBgColor(seed: string) {
36
+ let hash = 0
37
+ for (let i = 0; i < seed.length; i++) hash = seed.charCodeAt(i) + ((hash << 5) - hash)
38
+ return presetBgColors[Math.abs(hash) % presetBgColors.length]
39
+ }
40
+
41
+ export const authConfig: StartAuthJSConfig = {
42
+ session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60 },
43
+ pages: { signIn: '/login', error: '/login' },
44
+ secret: process.env.AUTH_SECRET,
45
+ trustHost: true,
46
+ cookies: {
47
+ sessionToken: {
48
+ name: 'auth.session-token',
49
+ options: {
50
+ httpOnly: true,
51
+ sameSite: 'lax',
52
+ path: '/',
53
+ secure: process.env.NODE_ENV === 'production',
54
+ domain: process.env.COOKIE_DOMAIN || undefined,
55
+ },
56
+ },
57
+ },
58
+ providers: [
59
+ CredentialsProvider({
60
+ name: 'credentials',
61
+ credentials: {
62
+ email: { label: 'Email', type: 'email' },
63
+ password: { label: 'Password', type: 'password' },
64
+ },
65
+ async authorize(credentials, request) {
66
+ const creds = credentials as { email: string; password: string; remember?: string; stayLoggedIn?: string }
67
+ if (!creds.email || !creds.password) return null
68
+
69
+ const user = await getDb().then(db => db.user.findUnique({
70
+ where: { email: creds.email },
71
+ select: { id: true, email: true, username: true, password: true, role: true, status: true },
72
+ }))
73
+
74
+ // 用户不存在:userId 为外键,无法记录日志(保持现状)
75
+ if (!user) return null
76
+
77
+ // 安全提取 IP/UA(start-authjs 传递的 request 类型可能不标准,失败不阻塞登录)
78
+ let ip: string | null = null
79
+ let userAgent: string | null = null
80
+ try {
81
+ const headers = request?.headers as { get?: (name: string) => string | null } | undefined
82
+ if (headers && typeof headers.get === 'function') {
83
+ ip = getClientIp(request as Request)
84
+ userAgent = headers.get('user-agent') || null
85
+ }
86
+ } catch {
87
+ // 忽略:IP/UA 仅用于日志,获取失败不影响登录
88
+ }
89
+
90
+ // 安全记录登录日志(DB 异常不阻塞登录流程)
91
+ const recordLog = async (success: boolean, message?: string, location?: string) => {
92
+ try {
93
+ await getDb().then(db => db.loginLog.create({
94
+ data: {
95
+ userId: user.id,
96
+ ip,
97
+ userAgent,
98
+ location: location ?? null,
99
+ success,
100
+ ...(message ? { message } : {}),
101
+ },
102
+ }))
103
+ } catch (err) {
104
+ console.error('[auth] 记录登录日志失败:', err)
105
+ }
106
+ }
107
+
108
+ // 账号状态异常:记录失败原因
109
+ if (user.status === 'BANNED') {
110
+ await recordLog(false, '账号已封禁')
111
+ return null
112
+ }
113
+ if (user.status === 'INACTIVE') {
114
+ await recordLog(false, '账号未激活')
115
+ return null
116
+ }
117
+ if (user.status !== 'ACTIVE') {
118
+ await recordLog(false, '账号状态异常')
119
+ return null
120
+ }
121
+
122
+ // 非管理员:admin 端专用拦截
123
+ if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
124
+ await recordLog(false, '非管理员账号')
125
+ return null
126
+ }
127
+
128
+ // 密码校验
129
+ const isValid = await bcrypt.compare(creds.password, user.password)
130
+ if (!isValid) {
131
+ await recordLog(false, '密码错误')
132
+ return null
133
+ }
134
+
135
+ // 登录成功:记录 IP/UA/地理位置
136
+ const location = await resolveIpLocation(ip)
137
+ await recordLog(true, undefined, location)
138
+ await getDb().then(db => db.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } }))
139
+
140
+ const remember = creds.remember === 'true'
141
+ const stayLoggedIn = creds.stayLoggedIn === 'true'
142
+ return {
143
+ id: user.id,
144
+ email: user.email,
145
+ name: user.username,
146
+ role: user.role,
147
+ remember,
148
+ stayLoggedIn,
149
+ }
150
+ },
151
+ }),
152
+ ],
153
+ callbacks: {
154
+ async jwt({ token, user, trigger }) {
155
+ // DB 查询失败(Prisma Client 未 generate / 字段缺失 / 连接异常)不应炸 jwt 流程
156
+ try {
157
+ if (user) {
158
+ token.role = (user as { role: string }).role
159
+ token.id = user.id
160
+ const dbUser = await getDb().then(db => db.user.findUnique({
161
+ where: { id: user.id },
162
+ select: { avatarConfig: true, bgShape: true, bgColor: true, username: true },
163
+ }))
164
+ if (dbUser) {
165
+ if (dbUser.avatarConfig) {
166
+ token.avatarConfig = dbUser.avatarConfig as Record<string, number>
167
+ token.bgShape = dbUser.bgShape ?? 'square'
168
+ token.bgColor = dbUser.bgColor ?? '#fff'
169
+ } else {
170
+ const avatarConfig = getRandomConfig()
171
+ const bgColor = pickBgColor(dbUser.username)
172
+ await getDb().then(db => db.user.update({
173
+ where: { id: user.id },
174
+ data: { avatarConfig, bgShape: 'square', bgColor },
175
+ }))
176
+ token.avatarConfig = avatarConfig as Record<string, number>
177
+ token.bgShape = 'square'
178
+ token.bgColor = bgColor
179
+ }
180
+ }
181
+ token.lastActivity = Date.now()
182
+ token.adminTimeout = (user as { stayLoggedIn?: boolean }).stayLoggedIn
183
+ ? 12 * 60 * 60 * 1000
184
+ : 30 * 60 * 1000
185
+ }
186
+ if (token.id && trigger === 'update') {
187
+ const dbUser = await getDb().then(db => db.user.findUnique({
188
+ where: { id: token.id as string },
189
+ select: { avatarConfig: true, bgShape: true, bgColor: true, username: true },
190
+ }))
191
+ if (dbUser) {
192
+ token.avatarConfig = (dbUser.avatarConfig ?? getRandomConfig()) as Record<string, number>
193
+ token.bgShape = dbUser.bgShape ?? 'square'
194
+ token.bgColor = dbUser.bgColor ?? pickBgColor(dbUser.username)
195
+ }
196
+ }
197
+ if (token.id && !user && trigger !== 'update') {
198
+ const dbUser = await getDb().then(db => db.user.findUnique({
199
+ where: { id: token.id as string },
200
+ select: { bgShape: true, bgColor: true, avatarConfig: true, username: true },
201
+ }))
202
+ if (dbUser) {
203
+ token.bgShape = dbUser.bgShape ?? 'square'
204
+ token.bgColor = dbUser.bgColor ?? pickBgColor(dbUser.username)
205
+ token.avatarConfig = (dbUser.avatarConfig ?? getRandomConfig()) as Record<string, number>
206
+ }
207
+ }
208
+ if (token.lastActivity && token.adminTimeout) {
209
+ token.lastActivity = Date.now()
210
+ }
211
+ return token
212
+ } catch (e) {
213
+ console.error('[auth][admin] jwt callback 查询失败,降级返回原 token:', e)
214
+ return token
215
+ }
216
+ },
217
+ async session({ session, token }) {
218
+ if (session.user) {
219
+ if (token.lastActivity && token.adminTimeout) {
220
+ const elapsed = Date.now() - (token.lastActivity as number)
221
+ if (elapsed > (token.adminTimeout as number)) {
222
+ return { ...session, user: null } as unknown as typeof session
223
+ }
224
+ }
225
+ // 用户存在性校验:token 指向的用户在 DB 不存在(DB 重置 / 用户被删 + 旧 JWT 残留)
226
+ // DB 查询失败时降级跳过(不返回 user:null 导致登出),避免 Prisma Client 问题连锁影响登录
227
+ if (token.id) {
228
+ try {
229
+ const exists = await getDb().then(db => db.user.findUnique({
230
+ where: { id: token.id as string },
231
+ select: { id: true },
232
+ }))
233
+ if (!exists) {
234
+ return { ...session, user: null } as unknown as typeof session
235
+ }
236
+ } catch (e) {
237
+ console.error('[auth][admin] session callback 用户存在性检查失败,跳过:', e)
238
+ }
239
+ }
240
+ const user = session.user as unknown as SessionUser
241
+ user.role = token.role as string
242
+ user.id = token.id as string
243
+ user.avatarConfig = token.avatarConfig as Record<string, number> | undefined
244
+ user.bgShape = token.bgShape as string | undefined
245
+ user.bgColor = token.bgColor as string | undefined
246
+ if (token.name) user.name = token.name as string | null
247
+ if (token.email) user.email = token.email as string | null
248
+ user.image = (token.picture as string | null) ?? null
249
+
250
+ try {
251
+ const sub = await getDb().then(db => db.subscription.findUnique({
252
+ where: { userId: token.id as string },
253
+ select: { status: true, plan: { select: { name: true } } },
254
+ }))
255
+ user.planLabel =
256
+ sub?.status === 'ACTIVE' && sub.plan.name === 'pro' ? 'pro' : 'free'
257
+ } catch {
258
+ user.planLabel = 'free'
259
+ }
260
+ }
261
+ return session
262
+ },
263
+ },
264
+ }
@@ -0,0 +1,41 @@
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
+ const DICE_BEAR_BASE = "https://api.dicebear.com/7.x";
19
+
20
+ const DICE_BEAR_STYLES = ["avataaars", "micah", "notionists", "lorelei", "adventurer"] as const;
21
+
22
+ type DiceBearStyle = (typeof DICE_BEAR_STYLES)[number];
23
+
24
+ export interface AvatarOptions {
25
+ style?: DiceBearStyle;
26
+ size?: number;
27
+ square?: boolean;
28
+ }
29
+
30
+ export function getBoringAvatarUrl(
31
+ seed: string,
32
+ options: AvatarOptions = {}
33
+ ): string {
34
+ const { style = "avataaars", size = 80 } = options;
35
+
36
+ return `${DICE_BEAR_BASE}/${style}/svg?seed=${encodeURIComponent(seed)}&size=${size}`;
37
+ }
38
+
39
+ export function getAvatarFallback(name: string): string {
40
+ return name.charAt(0).toUpperCase();
41
+ }
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import i18next, { type i18n as I18nInstance } from 'i18next'
3
+ import { initReactI18next } from 'react-i18next'
4
+ import LanguageDetector from 'i18next-browser-languagedetector'
5
+ import { resources, fallbackLng, defaultNs, supportedLngs } from './config'
6
+
7
+ let clientI18n: I18nInstance | null = null
8
+
9
+ export function getClientI18n(): I18nInstance {
10
+ if (!clientI18n) {
11
+ clientI18n = i18next.createInstance()
12
+ clientI18n
13
+ .use(initReactI18next)
14
+ .use(LanguageDetector)
15
+ .init({
16
+ fallbackLng,
17
+ defaultNS: defaultNs,
18
+ supportedLngs: [...supportedLngs],
19
+ resources,
20
+ interpolation: { escapeValue: false },
21
+ react: { useSuspense: false },
22
+ detection: {
23
+ order: ['querystring', 'cookie', 'localStorage', 'navigator'],
24
+ lookupQuerystring: 'lng',
25
+ lookupCookie: 'lng',
26
+ lookupLocalStorage: 'lng',
27
+ caches: ['cookie', 'localStorage'],
28
+ },
29
+ })
30
+ }
31
+ return clientI18n
32
+ }
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import zhCommon from './locales/zh/common.json'
3
+ import enCommon from './locales/en/common.json'
4
+ import zhUsers from './locales/zh/admin-users.json'
5
+ import enUsers from './locales/en/admin-users.json'
6
+ import zhAi from './locales/zh/admin-ai.json'
7
+ import enAi from './locales/en/admin-ai.json'
8
+ import zhBilling from './locales/zh/admin-billing.json'
9
+ import enBilling from './locales/en/admin-billing.json'
10
+ import zhEnterprise from './locales/zh/enterprise.json'
11
+ import enEnterprise from './locales/en/enterprise.json'
12
+
13
+ export const supportedLngs = ['zh', 'en'] as const
14
+ export type AppLocale = (typeof supportedLngs)[number]
15
+ export const fallbackLng: AppLocale = 'zh'
16
+ export const defaultNs = 'common'
17
+
18
+ export const resources = {
19
+ zh: {
20
+ common: zhCommon,
21
+ 'admin-users': zhUsers,
22
+ 'admin-ai': zhAi,
23
+ 'admin-billing': zhBilling,
24
+ enterprise: zhEnterprise,
25
+ },
26
+ en: {
27
+ common: enCommon,
28
+ 'admin-users': enUsers,
29
+ 'admin-ai': enAi,
30
+ 'admin-billing': enBilling,
31
+ enterprise: enEnterprise,
32
+ },
33
+ } as const
34
+
35
+ export const namespaces = Object.keys(resources.zh) as Array<keyof typeof resources.zh>
36
+ export type NamespaceKey = (typeof namespaces)[number]
37
+
38
+ export function isSupportedLocale(lng: string): lng is AppLocale {
39
+ return (supportedLngs as readonly string[]).includes(lng)
40
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { supportedLngs, fallbackLng, isSupportedLocale, type AppLocale } from './config'
3
+
4
+ export function detectLocale(request: Request, userLocale: string | null): AppLocale {
5
+ const url = new URL(request.url)
6
+ const queryLng = url.searchParams.get('lng')
7
+ if (queryLng && isSupportedLocale(queryLng)) return queryLng
8
+
9
+ const cookieHeader = request.headers.get('cookie') ?? ''
10
+ const cookieLng = parseCookieValue(cookieHeader, 'lng')
11
+ if (cookieLng && isSupportedLocale(cookieLng)) return cookieLng
12
+
13
+ if (userLocale && isSupportedLocale(userLocale)) return userLocale
14
+
15
+ const acceptLang = request.headers.get('accept-language')
16
+ if (acceptLang) {
17
+ const detected = matchBestLanguage(acceptLang)
18
+ if (detected) return detected
19
+ }
20
+
21
+ return fallbackLng
22
+ }
23
+
24
+ function parseCookieValue(cookieHeader: string, key: string): string | null {
25
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${key}=([^;]+)`))
26
+ return match ? match[1] : null
27
+ }
28
+
29
+ function matchBestLanguage(acceptLang: string): AppLocale | null {
30
+ const entries = acceptLang
31
+ .split(',')
32
+ .map((part) => {
33
+ const [lang, qPart] = part.trim().split(';q=')
34
+ return { lang: lang.trim(), q: qPart ? parseFloat(qPart) : 1 }
35
+ })
36
+ .sort((a, b) => b.q - a.q)
37
+
38
+ for (const entry of entries) {
39
+ const base = entry.lang.split('-')[0].toLowerCase()
40
+ if (isSupportedLocale(base)) return base as AppLocale
41
+ }
42
+ return null
43
+ }
@@ -0,0 +1,128 @@
1
+ {
2
+ "page": {
3
+ "title": "AI Providers",
4
+ "description": "Manage AI provider configurations, models, and limits",
5
+ "create": "Create Provider",
6
+ "edit": "Edit Provider"
7
+ },
8
+ "provider": {
9
+ "name": "Provider",
10
+ "kind": "Adapter Type",
11
+ "status": "Status",
12
+ "apiKey": "API Key",
13
+ "baseUrl": "Base URL",
14
+ "models": "Models",
15
+ "enabled": "Enabled",
16
+ "disabled": "Disabled",
17
+ "totalKeys": "Total Keys",
18
+ "monthlyUsage": "Monthly Usage",
19
+ "displayName": "Display Name",
20
+ "brandName": "Brand Name",
21
+ "description": "Description",
22
+ "category": "Category",
23
+ "defaultBaseUrl": "Default Base URL",
24
+ "keyUrl": "Key URL",
25
+ "docsUrl": "Docs URL",
26
+ "keyPlaceholder": "Key Placeholder",
27
+ "iconSlug": "Icon Slug",
28
+ "brandColor": "Brand Color",
29
+ "isLocal": "Local Provider",
30
+ "sortOrder": "Sort Order"
31
+ },
32
+ "action": {
33
+ "addProvider": "Add Provider",
34
+ "editProvider": "Edit Provider",
35
+ "testConnection": "Test Connection",
36
+ "manageModels": "Manage Models",
37
+ "toggleEnabled": "Toggle Enabled"
38
+ },
39
+ "model": {
40
+ "id": "Model ID",
41
+ "name": "Model Name",
42
+ "inputPrice": "Input Price",
43
+ "outputPrice": "Output Price",
44
+ "capabilities": "Capabilities",
45
+ "custom": "Custom",
46
+ "apiFetched": "API Fetched"
47
+ },
48
+ "form": {
49
+ "addProvider": "Add AI Provider",
50
+ "editProvider": "Edit AI Provider",
51
+ "basicInfo": "Basic Info",
52
+ "integrationConfig": "Integration Config",
53
+ "modelList": "Models",
54
+ "providerId": "Provider ID",
55
+ "providerIdDesc": "Business identifier, lowercase letters/numbers/dashes only, used for adapter routing.",
56
+ "providerIdEditNote": "Cannot be changed after creation",
57
+ "providerIdPlaceholder": "e.g. openai, deepseek",
58
+ "preview": "Preview",
59
+ "previewDesc": "Icon preview appears after filling in",
60
+ "displayName": "Display Name",
61
+ "displayNameDesc": "Chinese name shown on user-side card",
62
+ "brandName": "Brand Name",
63
+ "brandNameDesc": "English brand name (used for first-letter fallback)",
64
+ "description": "Description",
65
+ "descriptionDesc": "One-line provider description",
66
+ "category": "Category",
67
+ "adapterKind": "Adapter Type",
68
+ "adapterKindDesc": "Determines which SDK protocol to call",
69
+ "badge": "Badge Text",
70
+ "badgeDesc": "Label shown in top-right of card (e.g. 「Recommended」「Hot」「NEW」), leave empty to hide",
71
+ "badgePlaceholder": "e.g. Recommended",
72
+ "brandColor": "Brand Color",
73
+ "brandColorDesc": "Card accent color (hex)",
74
+ "iconSlug": "Icon Slug",
75
+ "iconSlugDesc": "Slug from @lobehub/icons (e.g. OpenAI, Anthropic), leave empty for first-letter fallback",
76
+ "iconSlugPlaceholder": "e.g. openai",
77
+ "defaultBaseUrl": "Default BaseURL",
78
+ "defaultBaseUrlDesc": "Used when user doesn't specify a custom endpoint",
79
+ "keyUrl": "API Key URL",
80
+ "docsUrl": "Docs URL",
81
+ "apikeyPlaceholder": "API Key Placeholder",
82
+ "localDeploy": "Local Deploy",
83
+ "localDeployDesc": "Check for local services like Ollama; API Key not required",
84
+ "platformEnabled": "Platform Enabled",
85
+ "platformEnabledDesc": "Disabled providers are hidden from unconfigured users; configured users keep access",
86
+ "sortOrder": "Sort Weight",
87
+ "sortOrderDesc": "Lower numbers appear first",
88
+ "modelListDesc": "Users select enabled models from this list (optional, can leave empty)",
89
+ "validation": {
90
+ "providerIdRequired": "Please fill in Provider ID",
91
+ "displayNameRequired": "Please fill in Display Name"
92
+ },
93
+ "toast": {
94
+ "created": "Provider created",
95
+ "updated": "Provider updated",
96
+ "saveFailed": "Save failed: {{error}}"
97
+ }
98
+ },
99
+ "table": {
100
+ "pageTitle": "AI Provider Management",
101
+ "pageDescription": "Manage {{count}} providers. Disabled providers are hidden from unconfigured users; configured users keep access. Unused providers can be hard-deleted.",
102
+ "addProvider": "Add Provider",
103
+ "columns": {
104
+ "provider": "Provider",
105
+ "adapterKind": "Model Adapter",
106
+ "category": "Category",
107
+ "badge": "Badge",
108
+ "modelCount": "Model Count",
109
+ "userConfigCount": "User Configs",
110
+ "platformEnabled": "Platform Enabled",
111
+ "actions": "Actions"
112
+ },
113
+ "enabledToggle": "Platform Enabled Toggle",
114
+ "edit": "Edit",
115
+ "delete": "Delete",
116
+ "deleteDisabled": "{{count}} users have configured this, cannot delete",
117
+ "deleteConfirm": {
118
+ "title": "Confirm delete provider 「{{name}}」?",
119
+ "description": "This action cannot be undone. The provider will be permanently removed. If you only want to temporarily disable, use the 「Disable」 function instead.",
120
+ "confirm": "Confirm Delete"
121
+ },
122
+ "toast": {
123
+ "updated": "Updated",
124
+ "deleted": "Provider deleted",
125
+ "deleteFailed": "Delete failed"
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "page": {
3
+ "title": "Billing",
4
+ "description": "Manage subscriptions, invoices, and revenue",
5
+ "selectSubmenu": "Please select a submenu on the left"
6
+ },
7
+ "subscription": {
8
+ "plan": "Plan",
9
+ "status": "Status",
10
+ "startDate": "Start Date",
11
+ "endDate": "End Date",
12
+ "amount": "Amount",
13
+ "currency": "Currency",
14
+ "interval": "Interval",
15
+ "totalUsers": "Total Users",
16
+ "overdue": "Overdue",
17
+ "active": "Active",
18
+ "cancelled": "Cancelled",
19
+ "trialing": "Trialing",
20
+ "workspace": "Workspace",
21
+ "seats": "Seats",
22
+ "expires": "Expires",
23
+ "detail": "Details",
24
+ "action": "Actions",
25
+ "empty": "No subscriptions yet",
26
+ "notFound": "Subscription not found",
27
+ "count": "{{count}} subscriptions",
28
+ "detailTitle": "Subscription Detail",
29
+ "periodStart": "Period Start",
30
+ "periodEnd": "Period End",
31
+ "providerSubscription": "Provider Subscription",
32
+ "back": "Back to List",
33
+ "invoicesTitle": "Invoices",
34
+ "invoiceDate": "Date",
35
+ "invoiceAmount": "Amount",
36
+ "invoiceStatus": "Status"
37
+ },
38
+ "invoice": {
39
+ "number": "Invoice Number",
40
+ "date": "Invoice Date",
41
+ "dueDate": "Due Date",
42
+ "total": "Total",
43
+ "paid": "Paid",
44
+ "unpaid": "Unpaid",
45
+ "view": "View Invoice",
46
+ "usd": "$",
47
+ "cny": "¥"
48
+ },
49
+ "action": {
50
+ "upgrade": "Upgrade Plan",
51
+ "downgrade": "Downgrade Plan",
52
+ "cancel": "Cancel Subscription",
53
+ "retryPayment": "Retry Payment",
54
+ "exportData": "Export Data"
55
+ }
56
+ }