@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,504 @@
1
+ import { createLazyFileRoute, Link, useLocation } from '@tanstack/react-router'
2
+ import { useServerFn } from '@tanstack/react-start'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { toast } from 'sonner'
5
+ import { useCallback, useEffect, useState } from 'react'
6
+ import type React from 'react'
7
+ import {
8
+ listRolesFn,
9
+ deleteRoleFn,
10
+ createRoleFn,
11
+ assignRoleFn,
12
+ unassignRoleFn,
13
+ listRoleAssignmentsFn,
14
+ } from '@/server/serverFns/permissions'
15
+ import { Badge } from '@/components/ui/badge'
16
+ import { Button } from '@/components/ui/button'
17
+ import {
18
+ Dialog,
19
+ DialogContent,
20
+ DialogDescription,
21
+ DialogFooter,
22
+ DialogHeader,
23
+ DialogTitle,
24
+ } from '@/components/ui/dialog'
25
+ import { Input } from '@/components/ui/input'
26
+ import { Label } from '@/components/ui/label'
27
+ import {
28
+ Select,
29
+ SelectContent,
30
+ SelectItem,
31
+ SelectTrigger,
32
+ SelectValue,
33
+ } from '@/components/ui/select'
34
+ import {
35
+ Table,
36
+ TableBody,
37
+ TableCell,
38
+ TableHead,
39
+ TableHeader,
40
+ TableRow,
41
+ } from '@/components/ui/table'
42
+
43
+ type ResourcePermission = Record<string, 'admin' | 'write' | 'read' | 'none'>
44
+
45
+ type RoleItem = {
46
+ id: string
47
+ name: string
48
+ permissions: ResourcePermission
49
+ isSystem: boolean
50
+ _count: { assignments: number }
51
+ }
52
+
53
+ type AssignmentItem = {
54
+ id: string
55
+ roleId: string
56
+ userId: string
57
+ user: {
58
+ id: string
59
+ username: string
60
+ email: string
61
+ }
62
+ createdAt: string | Date
63
+ }
64
+
65
+ type SearchUser = {
66
+ id: string
67
+ username: string
68
+ email: string
69
+ }
70
+
71
+ const ALL_RESOURCES: { key: string; labelZh: string; labelEn: string }[] = [
72
+ { key: 'knowledge', labelZh: '知识库', labelEn: 'Knowledge' },
73
+ { key: 'story', labelZh: '故事', labelEn: 'Story' },
74
+ { key: 'user', labelZh: '用户', labelEn: 'User' },
75
+ { key: 'setting', labelZh: '设置', labelEn: 'Setting' },
76
+ { key: 'billing', labelZh: '计费', labelEn: 'Billing' },
77
+ ]
78
+
79
+ const ALL_ACTIONS: { key: string; labelZh: string; labelEn: string }[] = [
80
+ { key: 'read', labelZh: '读', labelEn: 'Read' },
81
+ { key: 'write', labelZh: '写', labelEn: 'Write' },
82
+ { key: 'delete', labelZh: '删', labelEn: 'Delete' },
83
+ { key: 'publish', labelZh: '发布', labelEn: 'Publish' },
84
+ ]
85
+
86
+ const LEVELS: { value: 'none' | 'read' | 'write' | 'admin'; labelZh: string; labelEn: string }[] = [
87
+ { value: 'none', labelZh: '无', labelEn: 'None' },
88
+ { value: 'read', labelZh: '读', labelEn: 'Read' },
89
+ { value: 'write', labelZh: '写', labelEn: 'Write' },
90
+ { value: 'admin', labelZh: '管理', labelEn: 'Admin' },
91
+ ]
92
+
93
+ export const Route = createLazyFileRoute('/_nav/permissions')({
94
+ pendingComponent: () => (
95
+ <div className="flex h-64 items-center justify-center">
96
+ <div className="size-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
97
+ </div>
98
+ ),
99
+ component: PermissionsPage,
100
+ })
101
+
102
+ function CreateRoleDialog({
103
+ open,
104
+ onOpenChange,
105
+ workspaceId,
106
+ i18nLang,
107
+ onCreated,
108
+ }: {
109
+ open: boolean
110
+ onOpenChange: (v: boolean) => void
111
+ workspaceId: string
112
+ i18nLang: string
113
+ onCreated: () => void
114
+ }): React.ReactElement {
115
+ const { t } = useTranslation('enterprise')
116
+ const createRole = useServerFn(createRoleFn)
117
+ const [name, setName] = useState('')
118
+ const [permissions, setPermissions] = useState<ResourcePermission>({})
119
+ const isZh = i18nLang === 'zh'
120
+
121
+ useEffect(() => {
122
+ if (open) {
123
+ setName('')
124
+ setPermissions({})
125
+ }
126
+ }, [open])
127
+
128
+ const handleLevelChange = useCallback((resource: string, action: string, value: string) => {
129
+ const level = (value as 'none' | 'read' | 'write' | 'admin') || 'none'
130
+ setPermissions((prev) => ({ ...prev, [`${resource}:${action}`]: level }))
131
+ }, [])
132
+
133
+ const handleSubmit = async () => {
134
+ if (!name.trim()) {
135
+ toast.warning(t('permissions.toast.nameRequired'))
136
+ return
137
+ }
138
+ await createRole({
139
+ data: {
140
+ workspaceId,
141
+ name: name.trim(),
142
+ permissions,
143
+ },
144
+ })
145
+ toast.success(t('permissions.toast.created', { name: name.trim() }))
146
+ onOpenChange(false)
147
+ onCreated()
148
+ }
149
+
150
+ return (
151
+ <Dialog open={open} onOpenChange={onOpenChange}>
152
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
153
+ <DialogHeader>
154
+ <DialogTitle>{t('permissions.createRole')}</DialogTitle>
155
+ <DialogDescription>{t('permissions.createRoleDesc')}</DialogDescription>
156
+ </DialogHeader>
157
+
158
+ <div className="space-y-4">
159
+ <div className="grid gap-2">
160
+ <Label htmlFor="role-name">{t('permissions.roleName')}</Label>
161
+ <Input
162
+ id="role-name"
163
+ value={name}
164
+ onChange={(e) => setName(e.target.value)}
165
+ placeholder={t('permissions.roleNamePlaceholder')}
166
+ autoFocus
167
+ />
168
+ </div>
169
+
170
+ <div className="space-y-3">
171
+ <Label>{t('permissions.permissionsMatrix')}</Label>
172
+ {ALL_RESOURCES.map((resource) => (
173
+ <div key={resource.key} className="space-y-2">
174
+ <span className="text-sm font-medium">{isZh ? resource.labelZh : resource.labelEn}</span>
175
+ <div className="grid grid-cols-4 gap-2">
176
+ {ALL_ACTIONS.map((action) => (
177
+ <Select
178
+ key={action.key}
179
+ value={permissions[`${resource.key}:${action.key}`] || 'none'}
180
+ onValueChange={(value) => handleLevelChange(resource.key, action.key, value)}
181
+ >
182
+ <SelectTrigger className="h-8">
183
+ <SelectValue>
184
+ {isZh ? resource.labelEn : resource.labelZh} / {isZh ? action.labelZh : action.labelEn}
185
+ </SelectValue>
186
+ </SelectTrigger>
187
+ <SelectContent>
188
+ {LEVELS.map((level) => (
189
+ <SelectItem key={level.value} value={level.value}>
190
+ {isZh ? level.labelZh : level.labelEn}
191
+ </SelectItem>
192
+ ))}
193
+ </SelectContent>
194
+ </Select>
195
+ ))}
196
+ </div>
197
+ </div>
198
+ ))}
199
+ </div>
200
+ </div>
201
+
202
+ <DialogFooter>
203
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
204
+ {t('permissions.dialog.cancel')}
205
+ </Button>
206
+ <Button onClick={handleSubmit}>{t('permissions.dialog.create')}</Button>
207
+ </DialogFooter>
208
+ </DialogContent>
209
+ </Dialog>
210
+ )
211
+ }
212
+
213
+ function AssignDialog({
214
+ open,
215
+ onOpenChange,
216
+ role,
217
+ i18nLang,
218
+ onAssigned,
219
+ }: {
220
+ open: boolean
221
+ onOpenChange: (v: boolean) => void
222
+ role: RoleItem | null
223
+ i18nLang: string
224
+ onAssigned: () => void
225
+ }): React.ReactElement {
226
+ const { t } = useTranslation('enterprise')
227
+ const assignRole = useServerFn(assignRoleFn)
228
+ const unassignRole = useServerFn(unassignRoleFn)
229
+ const listAssignments = useServerFn(listRoleAssignmentsFn)
230
+ const [assignments, setAssignments] = useState<AssignmentItem[]>([])
231
+ const [searchUser, setSearchUser] = useState('')
232
+ const isZh = i18nLang === 'zh'
233
+
234
+ useEffect(() => {
235
+ if (open && role) {
236
+ listAssignments({ data: { roleId: role.id } })
237
+ .then((result) => setAssignments(result))
238
+ .catch(() => setAssignments([]))
239
+ }
240
+ }, [open, role, listAssignments])
241
+
242
+ const handleAssign = async () => {
243
+ if (!searchUser.trim() || !role) return
244
+ await assignRole({ data: { userId: searchUser.trim(), roleId: role.id } })
245
+ toast.success(t('permissions.toast.assigned'))
246
+ setSearchUser('')
247
+ onAssigned()
248
+ }
249
+
250
+ const handleUnassign = async (assignmentId: string) => {
251
+ await unassignRole({ data: { assignmentId } })
252
+ toast.success(t('permissions.toast.unassigned'))
253
+ setAssignments(assignments.filter((a) => a.id !== assignmentId))
254
+ }
255
+
256
+ return (
257
+ <Dialog open={open} onOpenChange={onOpenChange}>
258
+ <DialogContent className="max-w-lg">
259
+ <DialogHeader>
260
+ <DialogTitle>
261
+ {t('permissions.assign', { name: role?.name })}
262
+ </DialogTitle>
263
+ <DialogDescription>{t('permissions.assignDesc')}</DialogDescription>
264
+ </DialogHeader>
265
+
266
+ <div className="space-y-4">
267
+ <div className="flex gap-2">
268
+ <Input
269
+ value={searchUser}
270
+ onChange={(e) => setSearchUser(e.target.value)}
271
+ placeholder={t('permissions.searchUserPlaceholder')}
272
+ />
273
+ <Button onClick={handleAssign} disabled={!searchUser.trim()}>
274
+ {t('permissions.assignUser')}
275
+ </Button>
276
+ </div>
277
+
278
+ {assignments.length === 0 ? (
279
+ <p className="text-sm text-muted-foreground">{t('permissions.noAssignments')}</p>
280
+ ) : (
281
+ <Table>
282
+ <TableHeader>
283
+ <TableRow>
284
+ <TableHead>{isZh ? '用户' : 'User'}</TableHead>
285
+ <TableHead>{isZh ? '邮箱' : 'Email'}</TableHead>
286
+ <TableHead>{isZh ? '操作' : 'Actions'}</TableHead>
287
+ </TableRow>
288
+ </TableHeader>
289
+ <TableBody>
290
+ {assignments.map((a) => (
291
+ <TableRow key={a.id}>
292
+ <TableCell className="font-medium">{a.user?.username || a.userId}</TableCell>
293
+ <TableCell>{a.user?.email || '-'}</TableCell>
294
+ <TableCell>
295
+ <button
296
+ type="button"
297
+ className="text-destructive hover:underline"
298
+ onClick={() => handleUnassign(a.id)}
299
+ >
300
+ {t('permissions.unassign')}
301
+ </button>
302
+ </TableCell>
303
+ </TableRow>
304
+ ))}
305
+ </TableBody>
306
+ </Table>
307
+ )}
308
+ </div>
309
+
310
+ <DialogFooter>
311
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
312
+ {t('permissions.dialog.close')}
313
+ </Button>
314
+ </DialogFooter>
315
+ </DialogContent>
316
+ </Dialog>
317
+ )
318
+ }
319
+
320
+ function PermissionsPage(): React.ReactElement {
321
+ const { t, i18n } = useTranslation('enterprise')
322
+ const location = useLocation()
323
+ const selectedWorkspaceId =
324
+ (location.search as { workspaceId?: string }).workspaceId ??
325
+ (Route.useLoaderData() as {
326
+ workspaces: Array<{ id: string; name: string }>
327
+ }).workspaces[0]?.id ??
328
+ ''
329
+
330
+ const [roles, setRoles] = useState<RoleItem[]>([])
331
+ const [createDialogOpen, setCreateDialogOpen] = useState(false)
332
+ const [assignDialogOpen, setAssignDialogOpen] = useState(false)
333
+ const [selectedRole, setSelectedRole] = useState<RoleItem | null>(null)
334
+ const listRoles = useServerFn(listRolesFn)
335
+ const deleteRole = useServerFn(deleteRoleFn)
336
+
337
+ useEffect(() => {
338
+ if (selectedWorkspaceId) {
339
+ listRoles({ data: { workspaceId: selectedWorkspaceId } })
340
+ .then((result) => {
341
+ const data = result as { items: RoleItem[]; total: number; seeded: string }
342
+ setRoles(data.items)
343
+ })
344
+ .catch(() => setRoles([]))
345
+ }
346
+ }, [selectedWorkspaceId, listRoles])
347
+
348
+ const handleDelete = async (role: RoleItem) => {
349
+ if (role.isSystem) {
350
+ toast.info(t('permissions.toast.systemRole'))
351
+ return
352
+ }
353
+ await deleteRole({ data: { roleId: role.id } })
354
+ toast.success(t('permissions.toast.deleted', { name: role.name }))
355
+ setRoles(roles.filter((r) => r.id !== role.id))
356
+ }
357
+
358
+ const handleOpenAssign = (role: RoleItem) => {
359
+ setSelectedRole(role)
360
+ setAssignDialogOpen(true)
361
+ }
362
+
363
+ const PERMISSION_LEVELS: Record<string, () => string> = {
364
+ admin: () => t('permissions.level.admin'),
365
+ write: () => t('permissions.level.write'),
366
+ read: () => t('permissions.level.read'),
367
+ none: () => t('permissions.level.none'),
368
+ }
369
+
370
+ const workspaces = (Route.useLoaderData() as {
371
+ workspaces: Array<{ id: string; name: string }>
372
+ }).workspaces
373
+
374
+ return (
375
+ <div className="space-y-4">
376
+ <div>
377
+ <h1 className="text-2xl font-bold">{t('permissions.title')}</h1>
378
+ <p className="text-sm text-muted-foreground mt-1">{t('permissions.subtitle')}</p>
379
+ </div>
380
+
381
+ {workspaces.length > 0 && (
382
+ <div className="flex items-center gap-2">
383
+ <span className="text-sm text-muted-foreground">{t('permissions.workspaceLabel')}</span>
384
+ {workspaces.map((ws) => (
385
+ <Link
386
+ key={ws.id}
387
+ to="/permissions"
388
+ search={{ workspaceId: ws.id }}
389
+ className={`inline-flex rounded-md px-3 py-1 text-sm ${
390
+ ws.id === selectedWorkspaceId
391
+ ? 'bg-primary text-primary-foreground'
392
+ : 'bg-muted text-muted-foreground hover:bg-muted/80'
393
+ }`}
394
+ >
395
+ {ws.name}
396
+ </Link>
397
+ ))}
398
+ </div>
399
+ )}
400
+
401
+ {roles.length === 0 ? (
402
+ <div className="rounded-lg border border-border p-8 text-center text-muted-foreground">
403
+ {t('permissions.empty')}
404
+ </div>
405
+ ) : (
406
+ <div className="rounded-lg border border-border overflow-hidden">
407
+ <Table>
408
+ <TableHeader>
409
+ <TableRow>
410
+ <TableHead>{t('permissions.table.role')}</TableHead>
411
+ <TableHead>{t('permissions.table.type')}</TableHead>
412
+ <TableHead>{t('permissions.table.members')}</TableHead>
413
+ <TableHead>{t('permissions.table.permissions')}</TableHead>
414
+ <TableHead>{t('permissions.table.actions')}</TableHead>
415
+ </TableRow>
416
+ </TableHeader>
417
+ <TableBody>
418
+ {roles.map((role) => (
419
+ <TableRow key={role.id}>
420
+ <TableCell className="font-medium">{role.name}</TableCell>
421
+ <TableCell>
422
+ <Badge variant={role.isSystem ? 'default' : 'secondary'}>
423
+ {role.isSystem ? t('permissions.type.system') : t('permissions.type.custom')}
424
+ </Badge>
425
+ </TableCell>
426
+ <TableCell>{role._count?.assignments}</TableCell>
427
+ <TableCell>
428
+ <div className="flex flex-wrap gap-1">
429
+ {Object.entries(role.permissions || {}).map(([perm, level]) => (
430
+ <span
431
+ key={perm}
432
+ className="inline-flex rounded bg-muted px-1.5 py-0.5 text-xs"
433
+ >
434
+ {perm.split(':').pop()}:{PERMISSION_LEVELS[level]?.() ?? level}
435
+ </span>
436
+ ))}
437
+ </div>
438
+ </TableCell>
439
+ <TableCell>
440
+ <div className="flex gap-2 items-center">
441
+ <button
442
+ type="button"
443
+ className="text-primary hover:underline text-sm"
444
+ onClick={() => handleOpenAssign(role)}
445
+ >
446
+ {t('permissions.action.viewAssignments')}
447
+ </button>
448
+ <button
449
+ type="button"
450
+ className="text-destructive hover:underline text-sm"
451
+ onClick={() => handleDelete(role)}
452
+ disabled={role.isSystem}
453
+ >
454
+ {role.isSystem ? t('permissions.action.notAvailable') : t('permissions.action.delete')}
455
+ </button>
456
+ </div>
457
+ </TableCell>
458
+ </TableRow>
459
+ ))}
460
+ </TableBody>
461
+ </Table>
462
+ </div>
463
+ )}
464
+
465
+ {selectedWorkspaceId && (
466
+ <div className="flex justify-end">
467
+ <Button onClick={() => setCreateDialogOpen(true)}>{t('permissions.action.createRole')}</Button>
468
+ </div>
469
+ )}
470
+
471
+ <CreateRoleDialog
472
+ open={createDialogOpen}
473
+ onOpenChange={setCreateDialogOpen}
474
+ workspaceId={selectedWorkspaceId}
475
+ i18nLang={i18n.language}
476
+ onCreated={() => {
477
+ if (selectedWorkspaceId) {
478
+ listRoles({ data: { workspaceId: selectedWorkspaceId } })
479
+ .then((result) => {
480
+ const data = result as { items: RoleItem[] }
481
+ setRoles(data.items)
482
+ })
483
+ }
484
+ }}
485
+ />
486
+
487
+ <AssignDialog
488
+ open={assignDialogOpen}
489
+ onOpenChange={setAssignDialogOpen}
490
+ role={selectedRole}
491
+ i18nLang={i18n.language}
492
+ onAssigned={() => {
493
+ if (selectedWorkspaceId) {
494
+ listRoles({ data: { workspaceId: selectedWorkspaceId } })
495
+ .then((result) => {
496
+ const data = result as { items: RoleItem[] }
497
+ setRoles(data.items)
498
+ })
499
+ }
500
+ }}
501
+ />
502
+ </div>
503
+ )
504
+ }
@@ -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 { createFileRoute } from '@tanstack/react-router'
19
+ import { listRolesFn, listWorkspacesFn } from '@/server/serverFns/permissions'
20
+
21
+ export const Route = createFileRoute('/_nav/permissions')({
22
+ loader: async () => {
23
+ const workspaces = await listWorkspacesFn({ data: {} })
24
+ return { workspaces, roles: null }
25
+ },
26
+ })
@@ -0,0 +1,31 @@
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 { Outlet, createLazyFileRoute } from '@tanstack/react-router'
19
+ import Sidebar02 from '@/components/layout/sidebar-02'
20
+
21
+ export const Route = createLazyFileRoute('/_nav')({
22
+ component: DashboardLayout,
23
+ })
24
+
25
+ function DashboardLayout() {
26
+ return (
27
+ <Sidebar02>
28
+ <Outlet />
29
+ </Sidebar02>
30
+ )
31
+ }
@@ -0,0 +1,34 @@
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 { createFileRoute, redirect } from '@tanstack/react-router'
19
+ import { getSessionFn } from '@/server/serverFns/auth/session'
20
+
21
+ export const Route = createFileRoute('/_nav')({
22
+ beforeLoad: async ({ location }) => {
23
+ const session = await getSessionFn()
24
+ if (!session?.user?.id) {
25
+ throw redirect({
26
+ to: '/login',
27
+ search: { redirect: location.pathname + location.searchStr },
28
+ })
29
+ }
30
+ if (session.user.role !== 'ADMIN' && session.user.role !== 'SUPER_ADMIN') {
31
+ throw redirect({ to: '/login' })
32
+ }
33
+ },
34
+ })
@@ -0,0 +1,30 @@
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 { createLazyFileRoute } from '@tanstack/react-router'
19
+ import { useTranslation } from 'react-i18next'
20
+ import { Settings } from 'lucide-react'
21
+ import { PlaceholderPage } from '@/components/common/placeholder-page'
22
+
23
+ export const Route = createLazyFileRoute('/_nav/settings')({
24
+ component: SettingsPage,
25
+ })
26
+
27
+ function SettingsPage() {
28
+ const { t } = useTranslation('common')
29
+ return <PlaceholderPage title={t('settings.title')} icon={Settings} />
30
+ }
@@ -0,0 +1,20 @@
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 { createFileRoute } from '@tanstack/react-router'
19
+
20
+ export const Route = createFileRoute('/_nav/settings')({})