@allbluecn/admin-app 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/package.json +19 -17
  2. package/src/components/admin/admin-user-menu.tsx +4 -4
  3. package/src/components/admin/developer-log.tsx +16 -32
  4. package/src/components/ai-memory/memory-facts-panel.tsx +30 -70
  5. package/src/components/ai-prompts/preview-panel.tsx +1 -1
  6. package/src/components/ai-prompts/prompt-editor.tsx +8 -9
  7. package/src/components/ai-prompts/prompt-table.tsx +6 -6
  8. package/src/components/ai-prompts/section-editor.tsx +3 -3
  9. package/src/components/ai-prompts/variable-panel.tsx +2 -2
  10. package/src/components/ai-prompts/version-history.tsx +21 -41
  11. package/src/components/ai-provider/model-list-editor.tsx +2 -3
  12. package/src/components/ai-provider/provider-form.tsx +7 -7
  13. package/src/components/ai-provider/provider-table.tsx +17 -40
  14. package/src/components/auth/admin-login-form.tsx +7 -7
  15. package/src/components/auth/icon-input.tsx +2 -2
  16. package/src/components/auth/password-input.tsx +2 -2
  17. package/src/components/component/component-form-dialog.tsx +10 -18
  18. package/src/components/component/component-import-dialog.tsx +6 -6
  19. package/src/components/component/component-preview.tsx +311 -0
  20. package/src/components/component/component-table.tsx +3 -3
  21. package/src/components/dependency/changelog-dialog.tsx +2 -2
  22. package/src/components/dependency/columns.tsx +3 -3
  23. package/src/components/dependency/dependency-table.tsx +7 -7
  24. package/src/components/dependency/edit-dialog.tsx +8 -8
  25. package/src/components/invitations/invitations-page.tsx +26 -49
  26. package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
  27. package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
  28. package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
  29. package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -2
  30. package/src/components/layout/sidebar-02/index.tsx +1 -1
  31. package/src/components/layout/sidebar-02/nav-main.tsx +2 -3
  32. package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
  33. package/src/components/market/columns.tsx +4 -4
  34. package/src/components/market/market-app-form.tsx +6 -7
  35. package/src/components/market/market-app-list.tsx +14 -29
  36. package/src/components/monitoring/error-list.tsx +2 -2
  37. package/src/components/monitoring/metrics-stats.tsx +1 -1
  38. package/src/components/monitoring/request-log-table.tsx +2 -2
  39. package/src/components/oauth-providers/provider-page.tsx +20 -38
  40. package/src/components/permissions/assign-dialog.tsx +135 -0
  41. package/src/components/permissions/create-role-dialog.tsx +135 -0
  42. package/src/components/permissions/types.ts +49 -0
  43. package/src/components/user/columns.tsx +6 -6
  44. package/src/components/user/settings-activity-log.tsx +7 -7
  45. package/src/components/user/settings-profile/avatar-uploader.tsx +118 -0
  46. package/src/components/user/settings-profile/social-links.tsx +63 -0
  47. package/src/components/user/settings-profile.tsx +26 -125
  48. package/src/components/user/user-create-dialog.tsx +6 -6
  49. package/src/components/user/user-detail/error-boundary.tsx +19 -0
  50. package/src/components/user/user-detail/user-detail-dialogs.tsx +203 -0
  51. package/src/components/user/user-filters.tsx +4 -4
  52. package/src/lib/auth/page-gate.ts +28 -0
  53. package/src/lib/auth.config.ts +25 -9
  54. package/src/lib/i18n/detect.ts +1 -1
  55. package/src/lib/loader-serial.ts +14 -0
  56. package/src/providers/index.tsx +1 -1
  57. package/src/routeTree.gen.ts +67 -78
  58. package/src/routes/__root.tsx +4 -4
  59. package/src/routes/_nav/audit/route.lazy.tsx +1 -2
  60. package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
  61. package/src/routes/_nav/components/route.lazy.tsx +14 -34
  62. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
  63. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
  64. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
  65. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
  66. package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
  67. package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
  68. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
  69. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
  70. package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
  71. package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
  72. package/src/routes/_nav/monitoring/health/route.tsx +14 -0
  73. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
  74. package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
  75. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
  76. package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
  77. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
  78. package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
  79. package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
  80. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
  81. package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
  82. package/src/routes/_nav/permissions/route.tsx +1 -1
  83. package/src/routes/_nav/route.tsx +5 -13
  84. package/src/routes/_nav/sso/route.lazy.tsx +2 -3
  85. package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
  86. package/src/routes/_nav/users/-components/users-table.tsx +1 -1
  87. package/src/routes/_nav/users/route.lazy.tsx +1 -1
  88. package/src/routes/login/route.lazy.tsx +1 -1
  89. package/src/routes/setup/route.lazy.tsx +2 -2
  90. package/src/server/__tests__/admin-billing.test.ts +191 -16
  91. package/src/server/__tests__/compliance.test.ts +44 -7
  92. package/src/server/__tests__/permissions.test.ts +10 -5
  93. package/src/server/__tests__/prompt-defaults.test.ts +3 -3
  94. package/src/server/admin-ai-alerts.ts +2 -1
  95. package/src/server/admin-ai-usage.ts +2 -1
  96. package/src/server/ai-memory/store.ts +6 -2
  97. package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
  98. package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
  99. package/src/server/knowledge-public/github-sync.ts +2 -1
  100. package/src/server/license-issue.ts +2 -1
  101. package/src/server/middlewares/auth-rate-limit.ts +117 -0
  102. package/src/server/observability/health-check.ts +4 -1
  103. package/src/server/observability/metrics.ts +17 -15
  104. package/src/server/observability/request-middleware.ts +2 -1
  105. package/src/server/observability/timing.ts +14 -16
  106. package/src/server/serverFns/admin-billing.ts +107 -12
  107. package/src/server/serverFns/ai-memory/list-users.ts +2 -1
  108. package/src/server/serverFns/ai-prompts/activate.ts +2 -1
  109. package/src/server/serverFns/ai-prompts/get.ts +3 -2
  110. package/src/server/serverFns/ai-prompts/list.ts +3 -2
  111. package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
  112. package/src/server/serverFns/ai-prompts/save.ts +5 -3
  113. package/src/server/serverFns/ai-provider/create.ts +4 -1
  114. package/src/server/serverFns/ai-provider/delete.ts +4 -1
  115. package/src/server/serverFns/ai-provider/get.ts +4 -1
  116. package/src/server/serverFns/ai-provider/list.ts +28 -1
  117. package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
  118. package/src/server/serverFns/ai-provider/update.ts +4 -1
  119. package/src/server/serverFns/anyhub/market.ts +8 -1
  120. package/src/server/serverFns/audit.ts +14 -1
  121. package/src/server/serverFns/auth/setup.ts +3 -1
  122. package/src/server/serverFns/compliance.ts +32 -3
  123. package/src/server/serverFns/components/import.ts +69 -14
  124. package/src/server/serverFns/components/list.ts +4 -1
  125. package/src/server/serverFns/components/mutations.ts +9 -1
  126. package/src/server/serverFns/dependency/list.ts +28 -5
  127. package/src/server/serverFns/dependency/sync.ts +7 -2
  128. package/src/server/serverFns/dependency/update.ts +4 -1
  129. package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
  130. package/src/server/serverFns/developer-logs.ts +5 -1
  131. package/src/server/serverFns/invitations.ts +7 -1
  132. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
  133. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
  134. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
  135. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
  136. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
  137. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
  138. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  139. package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
  140. package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
  141. package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
  142. package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
  143. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  144. package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
  145. package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
  146. package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
  147. package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
  148. package/src/server/serverFns/licenses.ts +8 -2
  149. package/src/server/serverFns/monitoring/get-errors.ts +2 -1
  150. package/src/server/serverFns/monitoring/get-health.ts +2 -1
  151. package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
  152. package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
  153. package/src/server/serverFns/monitoring/report-error.ts +2 -1
  154. package/src/server/serverFns/monitoring/reset.ts +2 -1
  155. package/src/server/serverFns/oauth-providers.ts +4 -1
  156. package/src/server/serverFns/permissions.ts +18 -11
  157. package/src/server/serverFns/sentry/config.ts +11 -8
  158. package/src/server/serverFns/sso-config.ts +17 -2
  159. package/src/server/serverFns/user/create.ts +6 -1
  160. package/src/server/serverFns/user/delete.ts +6 -1
  161. package/src/server/serverFns/user/detail.ts +5 -2
  162. package/src/server/serverFns/user/list.ts +19 -4
  163. package/src/server/serverFns/user/reset-password.ts +10 -2
  164. package/src/server/serverFns/user/update-role.ts +10 -2
  165. package/src/server/serverFns/user/update-status.ts +10 -2
  166. package/src/styles/globals.css +4 -0
  167. package/src/components/data-table/data-table-pagination.tsx +0 -112
  168. package/src/components/data-table/data-table.tsx +0 -218
  169. package/src/components/data-table/table-detail-layout.tsx +0 -98
  170. package/src/components/ui/alert-dialog.tsx +0 -187
  171. package/src/components/ui/alert.tsx +0 -75
  172. package/src/components/ui/avatar.tsx +0 -106
  173. package/src/components/ui/badge.tsx +0 -51
  174. package/src/components/ui/button.tsx +0 -57
  175. package/src/components/ui/card.tsx +0 -102
  176. package/src/components/ui/checkbox.tsx +0 -28
  177. package/src/components/ui/collapsible.tsx +0 -19
  178. package/src/components/ui/dialog.tsx +0 -155
  179. package/src/components/ui/dropdown-menu.tsx +0 -267
  180. package/src/components/ui/field.tsx +0 -236
  181. package/src/components/ui/input-group.tsx +0 -158
  182. package/src/components/ui/input.tsx +0 -19
  183. package/src/components/ui/label.tsx +0 -17
  184. package/src/components/ui/pagination.tsx +0 -130
  185. package/src/components/ui/select.tsx +0 -234
  186. package/src/components/ui/separator.tsx +0 -22
  187. package/src/components/ui/sheet.tsx +0 -135
  188. package/src/components/ui/sidebar.tsx +0 -721
  189. package/src/components/ui/skeleton.tsx +0 -13
  190. package/src/components/ui/switch.tsx +0 -29
  191. package/src/components/ui/table.tsx +0 -115
  192. package/src/components/ui/textarea.tsx +0 -17
  193. package/src/components/ui/toast.tsx +0 -282
  194. package/src/components/ui/tooltip.tsx +0 -63
  195. package/src/hooks/use-mobile.ts +0 -19
  196. package/src/lib/avatar.ts +0 -41
  197. /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
  198. /package/src/routes/{api.sentry-test.ts → api/sentry-test.ts} +0 -0
@@ -0,0 +1,203 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+ import { m } from '@/paraglide/messages.js'
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from '@allbluecn/ui/dialog'
11
+ import {
12
+ AlertDialog,
13
+ AlertDialogAction,
14
+ AlertDialogCancel,
15
+ AlertDialogContent,
16
+ AlertDialogDescription,
17
+ AlertDialogFooter,
18
+ AlertDialogHeader,
19
+ AlertDialogTitle,
20
+ } from '@allbluecn/ui/alert-dialog'
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '@allbluecn/ui/select'
28
+ import { Input } from '@allbluecn/ui/input'
29
+ import { Label } from '@allbluecn/ui/label'
30
+ import { Button } from '@allbluecn/ui/button'
31
+
32
+ export type UserDialogState = { open: boolean; value: string }
33
+ export type UserDeleteDialogState = { open: boolean; confirmName: string }
34
+
35
+ type DialogProps = {
36
+ username: string
37
+ isPending: boolean
38
+ }
39
+
40
+ /** 角色变更对话框(提交逻辑经 onSubmit/onCancel 由页面注入,保持页面侧事务编排) */
41
+ export function UserRoleDialog({
42
+ open,
43
+ value,
44
+ username,
45
+ isPending,
46
+ onOpenChange,
47
+ onValueChange,
48
+ onSubmit,
49
+ onCancel,
50
+ }: DialogProps & {
51
+ open: boolean
52
+ value: string
53
+ onOpenChange: (open: boolean) => void
54
+ onValueChange: (value: string) => void
55
+ onSubmit: () => void
56
+ onCancel: () => void
57
+ }) {
58
+ return (
59
+ <Dialog open={open} onOpenChange={onOpenChange}>
60
+ <DialogContent className="sm:max-w-100">
61
+ <DialogHeader>
62
+ <DialogTitle>{m['admin-users_dialog_role_title']({ username })}</DialogTitle>
63
+ </DialogHeader>
64
+ <div className="py-4">
65
+ <Label>{m['admin-users_dialog_role_newRole']()}</Label>
66
+ <Select value={value} onValueChange={(v) => onValueChange(v ?? "")}>
67
+ <SelectTrigger><SelectValue /></SelectTrigger>
68
+ <SelectContent>
69
+ <SelectItem value="USER">{m['admin-users_role_user']()}</SelectItem>
70
+ <SelectItem value="ADMIN">{m['admin-users_role_admin']()}</SelectItem>
71
+ <SelectItem value="SUPER_ADMIN">{m['admin-users_role_superAdmin']()}</SelectItem>
72
+ </SelectContent>
73
+ </Select>
74
+ </div>
75
+ <DialogFooter>
76
+ <Button variant="outline" onClick={onCancel}>{m.common_button_cancel()}</Button>
77
+ <Button disabled={isPending} onClick={onSubmit}>{m.common_button_confirm()}</Button>
78
+ </DialogFooter>
79
+ </DialogContent>
80
+ </Dialog>
81
+ )
82
+ }
83
+
84
+ /** 状态变更对话框 */
85
+ export function UserStatusDialog({
86
+ open,
87
+ value,
88
+ username,
89
+ isPending,
90
+ onOpenChange,
91
+ onValueChange,
92
+ onSubmit,
93
+ onCancel,
94
+ }: DialogProps & {
95
+ open: boolean
96
+ value: string
97
+ onOpenChange: (open: boolean) => void
98
+ onValueChange: (value: string) => void
99
+ onSubmit: () => void
100
+ onCancel: () => void
101
+ }) {
102
+ return (
103
+ <Dialog open={open} onOpenChange={onOpenChange}>
104
+ <DialogContent className="sm:max-w-100">
105
+ <DialogHeader>
106
+ <DialogTitle>{m['admin-users_dialog_status_title']({ username })}</DialogTitle>
107
+ </DialogHeader>
108
+ <div className="py-4">
109
+ <Label>{m['admin-users_dialog_status_newStatus']()}</Label>
110
+ <Select value={value} onValueChange={(v) => onValueChange(v ?? "")}>
111
+ <SelectTrigger><SelectValue /></SelectTrigger>
112
+ <SelectContent>
113
+ <SelectItem value="ACTIVE">{m['admin-users_status_active']()}</SelectItem>
114
+ <SelectItem value="INACTIVE">{m['admin-users_status_inactive']()}</SelectItem>
115
+ <SelectItem value="BANNED">{m['admin-users_status_banned']()}</SelectItem>
116
+ </SelectContent>
117
+ </Select>
118
+ </div>
119
+ <DialogFooter>
120
+ <Button variant="outline" onClick={onCancel}>{m.common_button_cancel()}</Button>
121
+ <Button disabled={isPending} onClick={onSubmit}>{m.common_button_confirm()}</Button>
122
+ </DialogFooter>
123
+ </DialogContent>
124
+ </Dialog>
125
+ )
126
+ }
127
+
128
+ /** 密码重置对话框(表单提交) */
129
+ export function UserPasswordDialog({
130
+ open,
131
+ value,
132
+ username,
133
+ isPending,
134
+ onOpenChange,
135
+ onValueChange,
136
+ onSubmit,
137
+ onCancel,
138
+ }: DialogProps & {
139
+ open: boolean
140
+ value: string
141
+ onOpenChange: (open: boolean) => void
142
+ onValueChange: (value: string) => void
143
+ onSubmit: () => void
144
+ onCancel: () => void
145
+ }) {
146
+ return (
147
+ <Dialog open={open} onOpenChange={onOpenChange}>
148
+ <DialogContent className="sm:max-w-100">
149
+ <DialogHeader>
150
+ <DialogTitle>{m['admin-users_dialog_password_title']({ username })}</DialogTitle>
151
+ </DialogHeader>
152
+ <form onSubmit={(e) => { e.preventDefault(); onSubmit() }}>
153
+ <div className="py-4">
154
+ <Label>{m['admin-users_dialog_password_newPassword']()}</Label>
155
+ <Input type="password" required value={value} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onValueChange(e.target.value)} />
156
+ </div>
157
+ <DialogFooter>
158
+ <Button type="button" variant="outline" onClick={onCancel}>{m.common_button_cancel()}</Button>
159
+ <Button type="submit" disabled={isPending}>{m['admin-users_dialog_password_confirm']()}</Button>
160
+ </DialogFooter>
161
+ </form>
162
+ </DialogContent>
163
+ </Dialog>
164
+ )
165
+ }
166
+
167
+ /** 删除用户确认对话框(输入用户名确认) */
168
+ export function UserDeleteDialog({
169
+ open,
170
+ confirmName,
171
+ username,
172
+ isPending,
173
+ onOpenChange,
174
+ onConfirmNameChange,
175
+ onSubmit,
176
+ }: DialogProps & {
177
+ open: boolean
178
+ confirmName: string
179
+ onOpenChange: (open: boolean) => void
180
+ onConfirmNameChange: (value: string) => void
181
+ onSubmit: () => void
182
+ }) {
183
+ return (
184
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
185
+ <AlertDialogContent>
186
+ <AlertDialogHeader>
187
+ <AlertDialogTitle>{m['admin-users_dialog_delete_title']()}</AlertDialogTitle>
188
+ <AlertDialogDescription>
189
+ {m['admin-users_dialog_delete_description']({ username })}
190
+ </AlertDialogDescription>
191
+ </AlertDialogHeader>
192
+ <Input placeholder={m['admin-users_dialog_delete_placeholder']()} value={confirmName} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onConfirmNameChange(e.target.value)} />
193
+ <AlertDialogFooter>
194
+ <AlertDialogCancel>{m.common_button_cancel()}</AlertDialogCancel>
195
+ <AlertDialogAction
196
+ disabled={confirmName !== username || isPending}
197
+ onClick={onSubmit}
198
+ >{m['admin-users_dialog_delete_confirm']()}</AlertDialogAction>
199
+ </AlertDialogFooter>
200
+ </AlertDialogContent>
201
+ </AlertDialog>
202
+ )
203
+ }
@@ -15,18 +15,18 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { useNavigate, useSearch } from "@tanstack/react-router";
18
+ import { useNavigate } from "@tanstack/react-router";
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import { useCallback } from "react";
21
- import { Input } from "@/components/ui/input";
22
- import { Button } from "@/components/ui/button";
21
+ import { Input } from "@allbluecn/ui/input";
22
+ import { Button } from "@allbluecn/ui/button";
23
23
  import {
24
24
  Select,
25
25
  SelectContent,
26
26
  SelectItem,
27
27
  SelectTrigger,
28
28
  SelectValue,
29
- } from "@/components/ui/select";
29
+ } from "@allbluecn/ui/select";
30
30
  import { SearchIcon, XIcon } from "lucide-react";
31
31
 
32
32
  interface UserFiltersProps {
@@ -0,0 +1,28 @@
1
+ import { redirect } from '@tanstack/react-router'
2
+
3
+ interface SessionLike {
4
+ user?: { id?: string; role?: string | null } | null
5
+ }
6
+
7
+ /**
8
+ * admin「需登录页面」beforeLoad gate:未登录跳登录并携带回跳;
9
+ * 已登录但非管理员(ADMIN/SUPER_ADMIN)拒绝回登录页。
10
+ * session 取自 __root beforeLoad 注入的 context.session(勿在此重复调 getSessionFn)。
11
+ */
12
+ export function requireAdminPage(args: {
13
+ context: { session: SessionLike | null }
14
+ location: { pathname: string; searchStr: string }
15
+ }): void {
16
+ if (!args.context.session?.user?.id) {
17
+ throw redirect({
18
+ to: '/login',
19
+ search: { redirect: args.location.pathname + args.location.searchStr },
20
+ })
21
+ }
22
+ if (
23
+ args.context.session.user.role !== 'ADMIN' &&
24
+ args.context.session.user.role !== 'SUPER_ADMIN'
25
+ ) {
26
+ throw redirect({ to: '/login' })
27
+ }
28
+ }
@@ -22,6 +22,7 @@ import type { PrismaClient } from '@allbluecn/database'
22
22
  import bcrypt from 'bcryptjs'
23
23
  import { getRandomConfig, presetBgColors } from '@/components/settings/avatar/avatar-config'
24
24
  import { resolveIpLocation, getClientIp } from '@allbluecn/kernel'
25
+ import { isLoginBlocked, recordLoginFailure, clearLoginFailures } from '@/server/middlewares/auth-rate-limit'
25
26
 
26
27
  let _prisma: PrismaClient | null = null
27
28
  async function getDb(): Promise<PrismaClient> {
@@ -66,13 +67,7 @@ export const authConfig: StartAuthJSConfig = {
66
67
  const creds = credentials as { email: string; password: string; remember?: string; stayLoggedIn?: string }
67
68
  if (!creds.email || !creds.password) return null
68
69
 
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
70
+ const loginEmail = creds.email.toLowerCase().trim()
76
71
 
77
72
  // 安全提取 IP/UA(start-authjs 传递的 request 类型可能不标准,失败不阻塞登录)
78
73
  let ip: string | null = null
@@ -84,7 +79,26 @@ export const authConfig: StartAuthJSConfig = {
84
79
  userAgent = headers.get('user-agent') || null
85
80
  }
86
81
  } catch {
87
- // 忽略:IP/UA 仅用于日志,获取失败不影响登录
82
+ // 忽略:IP/UA 仅用于限流与日志,获取失败不影响登录
83
+ }
84
+
85
+ // SEC-09:登录失败限流(email+IP 维度,仅失败计数,成功清零)。
86
+ // 被限流返回 null——与密码错误不可区分,不暴露「被限流」细节(防枚举)。
87
+ // 测试环境豁免(NODE_ENV=test)在限流器内部处理,e2e 成功登录不受影响。
88
+ if (isLoginBlocked(loginEmail, ip)) return null
89
+
90
+ const user = await getDb().then(db => db.user.findUnique({
91
+ // 邮箱按原样查询:admin 建号/初始 setup 均原样大小写入库(zod .email() 不转
92
+ // 小写),归一化查询会静默锁死存量混合大小写账号。限流 key 的归一在
93
+ // auth-rate-limit 的 loginKey() 内部完成,与 DB 查询口径无关。
94
+ where: { email: creds.email },
95
+ select: { id: true, email: true, username: true, password: true, role: true, status: true },
96
+ }))
97
+
98
+ // 用户不存在:userId 为外键,无法记录日志(保持现状)
99
+ if (!user) {
100
+ recordLoginFailure(loginEmail, ip)
101
+ return null
88
102
  }
89
103
 
90
104
  // 安全记录登录日志(DB 异常不阻塞登录流程)
@@ -128,11 +142,13 @@ export const authConfig: StartAuthJSConfig = {
128
142
  // 密码校验
129
143
  const isValid = await bcrypt.compare(creds.password, user.password)
130
144
  if (!isValid) {
145
+ recordLoginFailure(loginEmail, ip)
131
146
  await recordLog(false, '密码错误')
132
147
  return null
133
148
  }
134
149
 
135
- // 登录成功:记录 IP/UA/地理位置
150
+ // 登录成功:清零失败计数 + 记录 IP/UA/地理位置
151
+ clearLoginFailures(loginEmail, ip)
136
152
  const location = await resolveIpLocation(ip)
137
153
  await recordLog(true, undefined, location)
138
154
  await getDb().then(db => db.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } }))
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- import { supportedLngs, fallbackLng, isSupportedLocale, type AppLocale } from './config'
2
+ import { fallbackLng, isSupportedLocale, type AppLocale } from './config'
3
3
 
4
4
  export function detectLocale(request: Request, userLocale: string | null): AppLocale {
5
5
  const url = new URL(request.url)
@@ -0,0 +1,14 @@
1
+ // loader 预取数据的 JSON 归一化:客户端 RPC 路径返回纯 JSON(Date → ISO 字符串),
2
+ // SSR loader 路径若保留 Date 会让同组件在两条路径下收到不同形态。
3
+ // 统一在 loader 出口归一为 JSON 形态,保证与既有 useState + 客户端 fetch 行为一致。
4
+ export type Jsonified<T> = T extends (infer U)[]
5
+ ? Jsonified<U>[]
6
+ : T extends Date
7
+ ? string
8
+ : T extends object
9
+ ? { [K in keyof T]: Jsonified<T[K]> }
10
+ : T
11
+
12
+ export function jsonify<T>(value: T): Jsonified<T> {
13
+ return JSON.parse(JSON.stringify(value)) as Jsonified<T>
14
+ }
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { ThemeProvider } from "next-themes";
20
20
  import { type ReactNode } from "react";
21
- import { TooltipProvider } from "@/components/ui/tooltip";
21
+ import { TooltipProvider } from "@allbluecn/ui/tooltip";
22
22
 
23
23
  export function Providers({
24
24
  children,