@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.
- package/package.json +19 -17
- package/src/components/admin/admin-user-menu.tsx +4 -4
- package/src/components/admin/developer-log.tsx +16 -32
- package/src/components/ai-memory/memory-facts-panel.tsx +30 -70
- package/src/components/ai-prompts/preview-panel.tsx +1 -1
- package/src/components/ai-prompts/prompt-editor.tsx +8 -9
- package/src/components/ai-prompts/prompt-table.tsx +6 -6
- package/src/components/ai-prompts/section-editor.tsx +3 -3
- package/src/components/ai-prompts/variable-panel.tsx +2 -2
- package/src/components/ai-prompts/version-history.tsx +21 -41
- package/src/components/ai-provider/model-list-editor.tsx +2 -3
- package/src/components/ai-provider/provider-form.tsx +7 -7
- package/src/components/ai-provider/provider-table.tsx +17 -40
- package/src/components/auth/admin-login-form.tsx +7 -7
- package/src/components/auth/icon-input.tsx +2 -2
- package/src/components/auth/password-input.tsx +2 -2
- package/src/components/component/component-form-dialog.tsx +10 -18
- package/src/components/component/component-import-dialog.tsx +6 -6
- package/src/components/component/component-preview.tsx +311 -0
- package/src/components/component/component-table.tsx +3 -3
- package/src/components/dependency/changelog-dialog.tsx +2 -2
- package/src/components/dependency/columns.tsx +3 -3
- package/src/components/dependency/dependency-table.tsx +7 -7
- package/src/components/dependency/edit-dialog.tsx +8 -8
- package/src/components/invitations/invitations-page.tsx +26 -49
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
- package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
- package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
- package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -2
- package/src/components/layout/sidebar-02/index.tsx +1 -1
- package/src/components/layout/sidebar-02/nav-main.tsx +2 -3
- package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
- package/src/components/market/columns.tsx +4 -4
- package/src/components/market/market-app-form.tsx +6 -7
- package/src/components/market/market-app-list.tsx +14 -29
- package/src/components/monitoring/error-list.tsx +2 -2
- package/src/components/monitoring/metrics-stats.tsx +1 -1
- package/src/components/monitoring/request-log-table.tsx +2 -2
- package/src/components/oauth-providers/provider-page.tsx +20 -38
- package/src/components/permissions/assign-dialog.tsx +135 -0
- package/src/components/permissions/create-role-dialog.tsx +135 -0
- package/src/components/permissions/types.ts +49 -0
- package/src/components/user/columns.tsx +6 -6
- package/src/components/user/settings-activity-log.tsx +7 -7
- package/src/components/user/settings-profile/avatar-uploader.tsx +118 -0
- package/src/components/user/settings-profile/social-links.tsx +63 -0
- package/src/components/user/settings-profile.tsx +26 -125
- package/src/components/user/user-create-dialog.tsx +6 -6
- package/src/components/user/user-detail/error-boundary.tsx +19 -0
- package/src/components/user/user-detail/user-detail-dialogs.tsx +203 -0
- package/src/components/user/user-filters.tsx +4 -4
- package/src/lib/auth/page-gate.ts +28 -0
- package/src/lib/auth.config.ts +25 -9
- package/src/lib/i18n/detect.ts +1 -1
- package/src/lib/loader-serial.ts +14 -0
- package/src/providers/index.tsx +1 -1
- package/src/routeTree.gen.ts +67 -78
- package/src/routes/__root.tsx +4 -4
- package/src/routes/_nav/audit/route.lazy.tsx +1 -2
- package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
- package/src/routes/_nav/components/route.lazy.tsx +14 -34
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
- package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
- package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
- package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
- package/src/routes/_nav/monitoring/health/route.tsx +14 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
- package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
- package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
- package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
- package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
- package/src/routes/_nav/permissions/route.tsx +1 -1
- package/src/routes/_nav/route.tsx +5 -13
- package/src/routes/_nav/sso/route.lazy.tsx +2 -3
- package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
- package/src/routes/_nav/users/-components/users-table.tsx +1 -1
- package/src/routes/_nav/users/route.lazy.tsx +1 -1
- package/src/routes/login/route.lazy.tsx +1 -1
- package/src/routes/setup/route.lazy.tsx +2 -2
- package/src/server/__tests__/admin-billing.test.ts +191 -16
- package/src/server/__tests__/compliance.test.ts +44 -7
- package/src/server/__tests__/permissions.test.ts +10 -5
- package/src/server/__tests__/prompt-defaults.test.ts +3 -3
- package/src/server/admin-ai-alerts.ts +2 -1
- package/src/server/admin-ai-usage.ts +2 -1
- package/src/server/ai-memory/store.ts +6 -2
- package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
- package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
- package/src/server/knowledge-public/github-sync.ts +2 -1
- package/src/server/license-issue.ts +2 -1
- package/src/server/middlewares/auth-rate-limit.ts +117 -0
- package/src/server/observability/health-check.ts +4 -1
- package/src/server/observability/metrics.ts +17 -15
- package/src/server/observability/request-middleware.ts +2 -1
- package/src/server/observability/timing.ts +14 -16
- package/src/server/serverFns/admin-billing.ts +107 -12
- package/src/server/serverFns/ai-memory/list-users.ts +2 -1
- package/src/server/serverFns/ai-prompts/activate.ts +2 -1
- package/src/server/serverFns/ai-prompts/get.ts +3 -2
- package/src/server/serverFns/ai-prompts/list.ts +3 -2
- package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
- package/src/server/serverFns/ai-prompts/save.ts +5 -3
- package/src/server/serverFns/ai-provider/create.ts +4 -1
- package/src/server/serverFns/ai-provider/delete.ts +4 -1
- package/src/server/serverFns/ai-provider/get.ts +4 -1
- package/src/server/serverFns/ai-provider/list.ts +28 -1
- package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
- package/src/server/serverFns/ai-provider/update.ts +4 -1
- package/src/server/serverFns/anyhub/market.ts +8 -1
- package/src/server/serverFns/audit.ts +14 -1
- package/src/server/serverFns/auth/setup.ts +3 -1
- package/src/server/serverFns/compliance.ts +32 -3
- package/src/server/serverFns/components/import.ts +69 -14
- package/src/server/serverFns/components/list.ts +4 -1
- package/src/server/serverFns/components/mutations.ts +9 -1
- package/src/server/serverFns/dependency/list.ts +28 -5
- package/src/server/serverFns/dependency/sync.ts +7 -2
- package/src/server/serverFns/dependency/update.ts +4 -1
- package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
- package/src/server/serverFns/developer-logs.ts +5 -1
- package/src/server/serverFns/invitations.ts +7 -1
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
- package/src/server/serverFns/licenses.ts +8 -2
- package/src/server/serverFns/monitoring/get-errors.ts +2 -1
- package/src/server/serverFns/monitoring/get-health.ts +2 -1
- package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
- package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
- package/src/server/serverFns/monitoring/report-error.ts +2 -1
- package/src/server/serverFns/monitoring/reset.ts +2 -1
- package/src/server/serverFns/oauth-providers.ts +4 -1
- package/src/server/serverFns/permissions.ts +18 -11
- package/src/server/serverFns/sentry/config.ts +11 -8
- package/src/server/serverFns/sso-config.ts +17 -2
- package/src/server/serverFns/user/create.ts +6 -1
- package/src/server/serverFns/user/delete.ts +6 -1
- package/src/server/serverFns/user/detail.ts +5 -2
- package/src/server/serverFns/user/list.ts +19 -4
- package/src/server/serverFns/user/reset-password.ts +10 -2
- package/src/server/serverFns/user/update-role.ts +10 -2
- package/src/server/serverFns/user/update-status.ts +10 -2
- package/src/styles/globals.css +4 -0
- package/src/components/data-table/data-table-pagination.tsx +0 -112
- package/src/components/data-table/data-table.tsx +0 -218
- package/src/components/data-table/table-detail-layout.tsx +0 -98
- package/src/components/ui/alert-dialog.tsx +0 -187
- package/src/components/ui/alert.tsx +0 -75
- package/src/components/ui/avatar.tsx +0 -106
- package/src/components/ui/badge.tsx +0 -51
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -102
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/collapsible.tsx +0 -19
- package/src/components/ui/dialog.tsx +0 -155
- package/src/components/ui/dropdown-menu.tsx +0 -267
- package/src/components/ui/field.tsx +0 -236
- package/src/components/ui/input-group.tsx +0 -158
- package/src/components/ui/input.tsx +0 -19
- package/src/components/ui/label.tsx +0 -17
- package/src/components/ui/pagination.tsx +0 -130
- package/src/components/ui/select.tsx +0 -234
- package/src/components/ui/separator.tsx +0 -22
- package/src/components/ui/sheet.tsx +0 -135
- package/src/components/ui/sidebar.tsx +0 -721
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/switch.tsx +0 -29
- package/src/components/ui/table.tsx +0 -115
- package/src/components/ui/textarea.tsx +0 -17
- package/src/components/ui/toast.tsx +0 -282
- package/src/components/ui/tooltip.tsx +0 -63
- package/src/hooks/use-mobile.ts +0 -19
- package/src/lib/avatar.ts +0 -41
- /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
- /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
|
|
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 "
|
|
22
|
-
import { Button } from "
|
|
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 "
|
|
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
|
+
}
|
package/src/lib/auth.config.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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() } }))
|
package/src/lib/i18n/detect.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
import {
|
|
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
|
+
}
|
package/src/providers/index.tsx
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { ThemeProvider } from "next-themes";
|
|
20
20
|
import { type ReactNode } from "react";
|
|
21
|
-
import { TooltipProvider } from "
|
|
21
|
+
import { TooltipProvider } from "@allbluecn/ui/tooltip";
|
|
22
22
|
|
|
23
23
|
export function Providers({
|
|
24
24
|
children,
|