@allbluecn/admin-app 0.2.0 → 0.2.1
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 +8 -8
- package/src/components/admin/admin-user-menu.tsx +12 -6
- package/src/components/admin/developer-log.tsx +42 -44
- package/src/components/ai-provider/model-list-editor.tsx +5 -4
- package/src/components/ai-provider/provider-form.tsx +63 -64
- package/src/components/ai-provider/provider-table.tsx +35 -36
- package/src/components/auth/admin-login-form.tsx +23 -24
- package/src/components/auth/oauth-section.tsx +3 -5
- package/src/components/auth/password-input.tsx +2 -3
- package/src/components/common/placeholder-page.tsx +2 -4
- package/src/components/component/component-form-dialog.tsx +48 -49
- package/src/components/component/component-import-dialog.tsx +5 -5
- package/src/components/component/component-table.tsx +13 -14
- package/src/components/data-table/data-table-pagination.tsx +4 -6
- package/src/components/data-table/table-detail-layout.tsx +2 -3
- package/src/components/dependency/changelog-dialog.tsx +3 -4
- package/src/components/dependency/columns.tsx +32 -24
- package/src/components/dependency/dependency-table.tsx +37 -29
- package/src/components/dependency/edit-dialog.tsx +24 -25
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +7 -7
- package/src/components/knowledge-public/compliance-form-dialog.tsx +13 -13
- package/src/components/knowledge-public/legal-form-dialog.tsx +51 -46
- package/src/components/layout/sidebar-02/app-sidebar.tsx +33 -34
- package/src/components/layout/sidebar-02/nav-main.tsx +1 -1
- package/src/components/market/columns.tsx +20 -21
- package/src/components/market/market-app-form.tsx +45 -38
- package/src/components/market/market-app-list.tsx +16 -17
- package/src/components/monitoring/error-list.tsx +18 -19
- package/src/components/monitoring/health-cards.tsx +11 -13
- package/src/components/monitoring/metrics-stats.tsx +11 -12
- package/src/components/monitoring/request-log-table.tsx +8 -9
- package/src/components/ui/pagination.tsx +0 -2
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/user/columns.tsx +9 -9
- package/src/components/user/settings-activity-log.tsx +39 -29
- package/src/components/user/settings-profile.tsx +36 -36
- package/src/components/user/user-create-dialog.tsx +4 -4
- package/src/components/user/user-filters.tsx +16 -18
- package/src/lib/i18n/config.ts +0 -32
- package/src/providers/index.tsx +6 -16
- package/src/routeTree.gen.ts +1199 -0
- package/src/routes/__root.tsx +9 -23
- package/src/routes/_nav/apps/route.lazy.tsx +2 -3
- package/src/routes/_nav/audit/route.lazy.tsx +10 -11
- package/src/routes/_nav/billing/invoices/route.lazy.tsx +6 -7
- package/src/routes/_nav/billing/route.lazy.tsx +3 -4
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +16 -17
- package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +9 -10
- package/src/routes/_nav/compliance/route.lazy.tsx +34 -35
- package/src/routes/_nav/components/route.lazy.tsx +6 -6
- package/src/routes/_nav/dashboard/route.lazy.tsx +3 -5
- package/src/routes/_nav/dependencies/route.lazy.tsx +7 -8
- package/src/routes/_nav/docs/route.lazy.tsx +2 -3
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +40 -41
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +32 -33
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +32 -33
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +24 -31
- package/src/routes/_nav/licenses/new/route.lazy.tsx +25 -26
- package/src/routes/_nav/licenses/route.lazy.tsx +17 -18
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +26 -27
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +11 -12
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +18 -20
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +12 -13
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +13 -15
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +12 -13
- package/src/routes/_nav/monitoring/route.lazy.tsx +8 -9
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +55 -57
- package/src/routes/_nav/permissions/route.lazy.tsx +48 -50
- package/src/routes/_nav/settings/route.lazy.tsx +2 -3
- package/src/routes/_nav/sso/route.lazy.tsx +20 -18
- package/src/routes/_nav/users/$userId/route.lazy.tsx +86 -82
- package/src/routes/_nav/users/route.lazy.tsx +4 -5
- package/src/routes/setup/route.lazy.tsx +15 -16
- package/src/start.ts +2 -2
- package/src/lib/i18n/client.ts +0 -32
- package/src/lib/i18n/locales/en/admin-ai.json +0 -128
- package/src/lib/i18n/locales/en/admin-billing.json +0 -56
- package/src/lib/i18n/locales/en/admin-users.json +0 -88
- package/src/lib/i18n/locales/en/common.json +0 -877
- package/src/lib/i18n/locales/en/enterprise.json +0 -208
- package/src/lib/i18n/locales/zh/admin-ai.json +0 -128
- package/src/lib/i18n/locales/zh/admin-billing.json +0 -56
- package/src/lib/i18n/locales/zh/admin-users.json +0 -88
- package/src/lib/i18n/locales/zh/common.json +0 -877
- package/src/lib/i18n/locales/zh/enterprise.json +0 -208
- package/src/lib/i18n/server.ts +0 -18
- package/src/lib/i18n/types.ts +0 -5
|
@@ -16,10 +16,8 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
19
20
|
import { useState, useTransition, useMemo } from 'react'
|
|
20
|
-
import { I18nextProvider } from 'react-i18next'
|
|
21
|
-
import { useTranslation } from 'react-i18next'
|
|
22
|
-
import { createServerI18n } from '@/lib/i18n/server'
|
|
23
21
|
import { formatTimestamp } from '@allbluecn/web-app/lib/datetime'
|
|
24
22
|
import { getUserDetailFn } from '@/server/serverFns/user/detail'
|
|
25
23
|
import { updateUserRoleFn } from '@/server/serverFns/user/update-role'
|
|
@@ -31,6 +29,19 @@ import { Badge } from '@/components/ui/badge'
|
|
|
31
29
|
import { Separator } from '@/components/ui/separator'
|
|
32
30
|
import { Button } from '@/components/ui/button'
|
|
33
31
|
import { UserAvatar } from '@allbluecn/web-app/user-avatar'
|
|
32
|
+
|
|
33
|
+
const STATUS_LABELS: Record<string, () => string> = {
|
|
34
|
+
active: m['admin-users_status_active'],
|
|
35
|
+
banned: m['admin-users_status_banned'],
|
|
36
|
+
inactive: m['admin-users_status_inactive'],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ROLE_LABELS: Record<string, () => string> = {
|
|
40
|
+
admin: m['admin-users_role_admin'],
|
|
41
|
+
guest: m['admin-users_role_guest'],
|
|
42
|
+
superAdmin: m['admin-users_role_superAdmin'],
|
|
43
|
+
user: m['admin-users_role_user'],
|
|
44
|
+
}
|
|
34
45
|
import {
|
|
35
46
|
Dialog,
|
|
36
47
|
DialogContent,
|
|
@@ -90,27 +101,20 @@ export const Route = createLazyFileRoute('/_nav/users/$userId')({
|
|
|
90
101
|
})
|
|
91
102
|
|
|
92
103
|
function AdminUserErrorBoundary({ error, reset }: { error: Error; reset: () => void }) {
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<I18nextProvider i18n={i18n}>
|
|
96
|
-
<AdminUserErrorBoundaryInner error={error} reset={reset} />
|
|
97
|
-
</I18nextProvider>
|
|
98
|
-
)
|
|
104
|
+
return <AdminUserErrorBoundaryInner error={error} reset={reset} />
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
function AdminUserErrorBoundaryInner({ error, reset }: { error: Error; reset: () => void }) {
|
|
102
|
-
const { t } = useTranslation('admin-users')
|
|
103
108
|
return (
|
|
104
109
|
<div className="flex flex-col items-center justify-center h-64 gap-3">
|
|
105
110
|
<AlertCircle className="size-8 text-destructive" />
|
|
106
|
-
<p className="text-sm text-muted-foreground">{
|
|
107
|
-
<Button variant="outline" size="sm" onClick={reset}>{
|
|
111
|
+
<p className="text-sm text-muted-foreground">{m['admin-users_detail_errorLoading']()}: {error.message}</p>
|
|
112
|
+
<Button variant="outline" size="sm" onClick={reset}>{m['admin-users_detail_retry']()}</Button>
|
|
108
113
|
</div>
|
|
109
114
|
)
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
function UserDetailPage() {
|
|
113
|
-
const { t } = useTranslation('admin-users')
|
|
114
118
|
const statusBadgeVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
|
115
119
|
ACTIVE: 'default',
|
|
116
120
|
INACTIVE: 'secondary',
|
|
@@ -124,7 +128,7 @@ function UserDetailPage() {
|
|
|
124
128
|
if (notFound) {
|
|
125
129
|
return (
|
|
126
130
|
<div className="flex h-64 items-center justify-center">
|
|
127
|
-
<p className="text-muted-foreground">{
|
|
131
|
+
<p className="text-muted-foreground">{m['admin-users_detail_userNotFound']()}</p>
|
|
128
132
|
</div>
|
|
129
133
|
)
|
|
130
134
|
}
|
|
@@ -141,8 +145,8 @@ function UserDetailPage() {
|
|
|
141
145
|
if (updated) setUser(updated)
|
|
142
146
|
})
|
|
143
147
|
|
|
144
|
-
const getRoleLabel = (role: string) =>
|
|
145
|
-
const getStatusLabel = (status: string) =>
|
|
148
|
+
const getRoleLabel = (role: string) => (ROLE_LABELS[role] ?? (() => role))()
|
|
149
|
+
const getStatusLabel = (status: string) => (STATUS_LABELS[status] ?? (() => status))()
|
|
146
150
|
|
|
147
151
|
return (
|
|
148
152
|
<>
|
|
@@ -176,21 +180,21 @@ function UserDetailPage() {
|
|
|
176
180
|
<SettingsProfile
|
|
177
181
|
readOnly
|
|
178
182
|
profile={{ name: user.username, email: user.email }}
|
|
179
|
-
title={
|
|
180
|
-
description={
|
|
183
|
+
title={m['admin-users_detail_profile_title']()}
|
|
184
|
+
description={m['admin-users_detail_profile_description']()}
|
|
181
185
|
actions={
|
|
182
186
|
<>
|
|
183
187
|
<Button variant="outline" size="sm" onClick={() => setRoleDialog({ open: true, value: user.role })}>
|
|
184
|
-
{
|
|
188
|
+
{m['admin-users_action_changeRole']()}
|
|
185
189
|
</Button>
|
|
186
190
|
<Button variant="outline" size="sm" onClick={() => setStatusDialog({ open: true, value: user.status })}>
|
|
187
|
-
{
|
|
191
|
+
{m['admin-users_action_changeStatus']()}
|
|
188
192
|
</Button>
|
|
189
193
|
<Button variant="outline" size="sm" onClick={() => setPasswordDialog({ open: true, value: '' })}>
|
|
190
|
-
{
|
|
194
|
+
{m['admin-users_action_resetPassword']()}
|
|
191
195
|
</Button>
|
|
192
196
|
<Button variant="destructive" size="sm" onClick={() => setDeleteDialog({ open: true, confirmName: '' })}>
|
|
193
|
-
{
|
|
197
|
+
{m.common_button_delete()}
|
|
194
198
|
</Button>
|
|
195
199
|
</>
|
|
196
200
|
}
|
|
@@ -198,22 +202,22 @@ function UserDetailPage() {
|
|
|
198
202
|
|
|
199
203
|
<Card className="shadow-xs">
|
|
200
204
|
<CardHeader>
|
|
201
|
-
<CardTitle>{
|
|
202
|
-
<CardDescription>{
|
|
205
|
+
<CardTitle>{m['admin-users_detail_account_title']()}</CardTitle>
|
|
206
|
+
<CardDescription>{m['admin-users_detail_account_description']()}</CardDescription>
|
|
203
207
|
</CardHeader>
|
|
204
208
|
<CardContent>
|
|
205
209
|
<div className="space-y-2 text-sm">
|
|
206
210
|
<div className="flex justify-between rounded-lg border p-3">
|
|
207
|
-
<span className="text-muted-foreground">{
|
|
211
|
+
<span className="text-muted-foreground">{m['admin-users_detail_account_createdAt']()}</span>
|
|
208
212
|
<span>{formatTimestamp(user.createdAt)}</span>
|
|
209
213
|
</div>
|
|
210
214
|
<div className="flex justify-between rounded-lg border p-3">
|
|
211
|
-
<span className="text-muted-foreground">{
|
|
215
|
+
<span className="text-muted-foreground">{m['admin-users_detail_account_updatedAt']()}</span>
|
|
212
216
|
<span>{formatTimestamp(user.updatedAt)}</span>
|
|
213
217
|
</div>
|
|
214
218
|
<div className="flex justify-between rounded-lg border p-3">
|
|
215
|
-
<span className="text-muted-foreground">{
|
|
216
|
-
<span>{user.lastLoginAt ? formatTimestamp(user.lastLoginAt) :
|
|
219
|
+
<span className="text-muted-foreground">{m['admin-users_detail_account_lastLogin']()}</span>
|
|
220
|
+
<span>{user.lastLoginAt ? formatTimestamp(user.lastLoginAt) : m['admin-users_detail_account_neverLoggedIn']()}</span>
|
|
217
221
|
</div>
|
|
218
222
|
</div>
|
|
219
223
|
</CardContent>
|
|
@@ -221,8 +225,8 @@ function UserDetailPage() {
|
|
|
221
225
|
|
|
222
226
|
<Card className="shadow-xs">
|
|
223
227
|
<CardHeader>
|
|
224
|
-
<CardTitle>{
|
|
225
|
-
<CardDescription>{
|
|
228
|
+
<CardTitle>{m['admin-users_detail_stats_title']()}</CardTitle>
|
|
229
|
+
<CardDescription>{m['admin-users_detail_stats_description']()}</CardDescription>
|
|
226
230
|
</CardHeader>
|
|
227
231
|
<CardContent>
|
|
228
232
|
<div className="grid grid-cols-2 gap-3">
|
|
@@ -230,28 +234,28 @@ function UserDetailPage() {
|
|
|
230
234
|
<FileTextIcon className="h-4 w-4 text-muted-foreground" />
|
|
231
235
|
<div>
|
|
232
236
|
<div className="text-lg font-semibold">{user._count.prds}</div>
|
|
233
|
-
<div className="text-xs text-muted-foreground">{
|
|
237
|
+
<div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_prds']()}</div>
|
|
234
238
|
</div>
|
|
235
239
|
</div>
|
|
236
240
|
<div className="flex items-center gap-2 rounded-lg border p-3">
|
|
237
241
|
<Package2Icon className="h-4 w-4 text-muted-foreground" />
|
|
238
242
|
<div>
|
|
239
243
|
<div className="text-lg font-semibold">{user._count.anyhubApps}</div>
|
|
240
|
-
<div className="text-xs text-muted-foreground">{
|
|
244
|
+
<div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_apps']()}</div>
|
|
241
245
|
</div>
|
|
242
246
|
</div>
|
|
243
247
|
<div className="flex items-center gap-2 rounded-lg border p-3">
|
|
244
248
|
<ShoppingBagIcon className="h-4 w-4 text-muted-foreground" />
|
|
245
249
|
<div>
|
|
246
250
|
<div className="text-lg font-semibold">{user._count.anyhubMarketApps}</div>
|
|
247
|
-
<div className="text-xs text-muted-foreground">{
|
|
251
|
+
<div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_marketApps']()}</div>
|
|
248
252
|
</div>
|
|
249
253
|
</div>
|
|
250
254
|
<div className="flex items-center gap-2 rounded-lg border p-3">
|
|
251
255
|
<ShieldCheckIcon className="h-4 w-4 text-muted-foreground" />
|
|
252
256
|
<div>
|
|
253
257
|
<div className="text-lg font-semibold">{user._count.reviews}</div>
|
|
254
|
-
<div className="text-xs text-muted-foreground">{
|
|
258
|
+
<div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_reviews']()}</div>
|
|
255
259
|
</div>
|
|
256
260
|
</div>
|
|
257
261
|
</div>
|
|
@@ -266,23 +270,23 @@ function UserDetailPage() {
|
|
|
266
270
|
id: log.id,
|
|
267
271
|
action: 'login',
|
|
268
272
|
type: 'login',
|
|
269
|
-
description: log.success ?
|
|
273
|
+
description: log.success ? m['admin-users_detail_loginLogs_success']() : m['admin-users_detail_loginLogs_failure'](),
|
|
270
274
|
ipAddress: log.ip || '',
|
|
271
275
|
location: log.location || '',
|
|
272
276
|
device: formatParsedUA(parseUserAgent(log.userAgent)),
|
|
273
277
|
timestamp: new Date(log.createdAt),
|
|
274
278
|
status: log.success ? 'success' : 'failed',
|
|
275
|
-
reason: log.success ? undefined : log.message ||
|
|
279
|
+
reason: log.success ? undefined : log.message || m['admin-users_detail_loginLogs_unknownReason'](),
|
|
276
280
|
}))}
|
|
277
|
-
title={
|
|
278
|
-
description={
|
|
279
|
-
onExport={async (filters) => {
|
|
281
|
+
title={m['admin-users_detail_loginLogs_title']()}
|
|
282
|
+
description={m['admin-users_detail_loginLogs_description']()}
|
|
283
|
+
onExport={async (filters: any) => {
|
|
280
284
|
const rows = user.loginLogs
|
|
281
285
|
.filter((log: any) => {
|
|
282
286
|
if (filters.type && filters.type !== 'login') return false
|
|
283
287
|
if (
|
|
284
288
|
filters.search &&
|
|
285
|
-
!`${log.success ?
|
|
289
|
+
!`${log.success ? m['admin-users_detail_loginLogs_success']() : m['admin-users_detail_loginLogs_failure']()} ${log.ip || ''}`
|
|
286
290
|
.toLowerCase()
|
|
287
291
|
.includes(filters.search.toLowerCase())
|
|
288
292
|
)
|
|
@@ -293,8 +297,8 @@ function UserDetailPage() {
|
|
|
293
297
|
const parsed = parseUserAgent(log.userAgent)
|
|
294
298
|
return [
|
|
295
299
|
new Date(log.createdAt).toISOString(),
|
|
296
|
-
log.success ?
|
|
297
|
-
log.success ? '' : log.message ||
|
|
300
|
+
log.success ? m['admin-users_detail_loginLogs_success']() : m['admin-users_detail_loginLogs_failure'](),
|
|
301
|
+
log.success ? '' : log.message || m['admin-users_detail_loginLogs_unknownReason'](),
|
|
298
302
|
log.ip || '',
|
|
299
303
|
log.location || '',
|
|
300
304
|
parsed.browser,
|
|
@@ -304,7 +308,7 @@ function UserDetailPage() {
|
|
|
304
308
|
.map((v: string) => `"${(v || '').replace(/"/g, '""')}"`)
|
|
305
309
|
.join(',')
|
|
306
310
|
})
|
|
307
|
-
const csv = [
|
|
311
|
+
const csv = [m['admin-users_detail_loginLogs_csvHeader'](), ...rows].join('\n')
|
|
308
312
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
309
313
|
const url = URL.createObjectURL(blob)
|
|
310
314
|
const a = document.createElement('a')
|
|
@@ -312,118 +316,118 @@ function UserDetailPage() {
|
|
|
312
316
|
a.download = `user-${user.username}-login-logs.csv`
|
|
313
317
|
a.click()
|
|
314
318
|
URL.revokeObjectURL(url)
|
|
315
|
-
toast.success(
|
|
319
|
+
toast.success(m['admin-users_detail_loginLogs_exportSuccess']())
|
|
316
320
|
}}
|
|
317
321
|
/>
|
|
318
322
|
</div>
|
|
319
323
|
</div>
|
|
320
324
|
</TableDetailLayout>
|
|
321
325
|
|
|
322
|
-
<Dialog open={roleDialog.open} onOpenChange={(o) => setRoleDialog((d) => ({ ...d, open: o }))}>
|
|
326
|
+
<Dialog open={roleDialog.open} onOpenChange={(o: boolean) => setRoleDialog((d) => ({ ...d, open: o }))}>
|
|
323
327
|
<DialogContent className="sm:max-w-100">
|
|
324
328
|
<DialogHeader>
|
|
325
|
-
<DialogTitle>{
|
|
329
|
+
<DialogTitle>{m['admin-users_dialog_role_title']({ username: user.username })}</DialogTitle>
|
|
326
330
|
</DialogHeader>
|
|
327
331
|
<div className="py-4">
|
|
328
|
-
<Label>{
|
|
329
|
-
<Select value={roleDialog.value} onValueChange={(v) => setRoleDialog((d) => ({ ...d, value: v }))}>
|
|
332
|
+
<Label>{m['admin-users_dialog_role_newRole']()}</Label>
|
|
333
|
+
<Select value={roleDialog.value} onValueChange={(v: string) => setRoleDialog((d) => ({ ...d, value: v }))}>
|
|
330
334
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
331
335
|
<SelectContent>
|
|
332
|
-
<SelectItem value="USER">{
|
|
333
|
-
<SelectItem value="ADMIN">{
|
|
334
|
-
<SelectItem value="SUPER_ADMIN">{
|
|
336
|
+
<SelectItem value="USER">{m['admin-users_role_user']()}</SelectItem>
|
|
337
|
+
<SelectItem value="ADMIN">{m['admin-users_role_admin']()}</SelectItem>
|
|
338
|
+
<SelectItem value="SUPER_ADMIN">{m['admin-users_role_superAdmin']()}</SelectItem>
|
|
335
339
|
</SelectContent>
|
|
336
340
|
</Select>
|
|
337
341
|
</div>
|
|
338
342
|
<DialogFooter>
|
|
339
|
-
<Button variant="outline" onClick={() => setRoleDialog((d) => ({ ...d, open: false }))}>{
|
|
343
|
+
<Button variant="outline" onClick={() => setRoleDialog((d) => ({ ...d, open: false }))}>{m.common_button_cancel()}</Button>
|
|
340
344
|
<Button disabled={isPending} onClick={() => startTransition(async () => {
|
|
341
345
|
try {
|
|
342
346
|
await updateUserRoleFn({ data: { userId: user.id, role: roleDialog.value as 'USER' | 'ADMIN' | 'SUPER_ADMIN' } })
|
|
343
|
-
toast.success(
|
|
347
|
+
toast.success(m['admin-users_toast_roleUpdated']())
|
|
344
348
|
setRoleDialog((d) => ({ ...d, open: false }))
|
|
345
349
|
await refetchUser()
|
|
346
|
-
} catch { toast.error(
|
|
347
|
-
})}>{
|
|
350
|
+
} catch { toast.error(m['admin-users_toast_updateFailed']()) }
|
|
351
|
+
})}>{m.common_button_confirm()}</Button>
|
|
348
352
|
</DialogFooter>
|
|
349
353
|
</DialogContent>
|
|
350
354
|
</Dialog>
|
|
351
355
|
|
|
352
|
-
<Dialog open={statusDialog.open} onOpenChange={(o) => setStatusDialog((d) => ({ ...d, open: o }))}>
|
|
356
|
+
<Dialog open={statusDialog.open} onOpenChange={(o: boolean) => setStatusDialog((d) => ({ ...d, open: o }))}>
|
|
353
357
|
<DialogContent className="sm:max-w-100">
|
|
354
358
|
<DialogHeader>
|
|
355
|
-
<DialogTitle>{
|
|
359
|
+
<DialogTitle>{m['admin-users_dialog_status_title']({ username: user.username })}</DialogTitle>
|
|
356
360
|
</DialogHeader>
|
|
357
361
|
<div className="py-4">
|
|
358
|
-
<Label>{
|
|
359
|
-
<Select value={statusDialog.value} onValueChange={(v) => setStatusDialog((d) => ({ ...d, value: v }))}>
|
|
362
|
+
<Label>{m['admin-users_dialog_status_newStatus']()}</Label>
|
|
363
|
+
<Select value={statusDialog.value} onValueChange={(v: string) => setStatusDialog((d) => ({ ...d, value: v }))}>
|
|
360
364
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
361
365
|
<SelectContent>
|
|
362
|
-
<SelectItem value="ACTIVE">{
|
|
363
|
-
<SelectItem value="INACTIVE">{
|
|
364
|
-
<SelectItem value="BANNED">{
|
|
366
|
+
<SelectItem value="ACTIVE">{m['admin-users_status_active']()}</SelectItem>
|
|
367
|
+
<SelectItem value="INACTIVE">{m['admin-users_status_inactive']()}</SelectItem>
|
|
368
|
+
<SelectItem value="BANNED">{m['admin-users_status_banned']()}</SelectItem>
|
|
365
369
|
</SelectContent>
|
|
366
370
|
</Select>
|
|
367
371
|
</div>
|
|
368
372
|
<DialogFooter>
|
|
369
|
-
<Button variant="outline" onClick={() => setStatusDialog((d) => ({ ...d, open: false }))}>{
|
|
373
|
+
<Button variant="outline" onClick={() => setStatusDialog((d) => ({ ...d, open: false }))}>{m.common_button_cancel()}</Button>
|
|
370
374
|
<Button disabled={isPending} onClick={() => startTransition(async () => {
|
|
371
375
|
try {
|
|
372
376
|
await updateUserStatusFn({ data: { userId: user.id, status: statusDialog.value as 'ACTIVE' | 'INACTIVE' | 'BANNED' } })
|
|
373
|
-
toast.success(
|
|
377
|
+
toast.success(m['admin-users_toast_statusUpdated']())
|
|
374
378
|
setStatusDialog((d) => ({ ...d, open: false }))
|
|
375
379
|
await refetchUser()
|
|
376
|
-
} catch { toast.error(
|
|
377
|
-
})}>{
|
|
380
|
+
} catch { toast.error(m['admin-users_toast_updateFailed']()) }
|
|
381
|
+
})}>{m.common_button_confirm()}</Button>
|
|
378
382
|
</DialogFooter>
|
|
379
383
|
</DialogContent>
|
|
380
384
|
</Dialog>
|
|
381
385
|
|
|
382
|
-
<Dialog open={passwordDialog.open} onOpenChange={(o) => setPasswordDialog((d) => ({ ...d, open: o }))}>
|
|
386
|
+
<Dialog open={passwordDialog.open} onOpenChange={(o: boolean) => setPasswordDialog((d) => ({ ...d, open: o }))}>
|
|
383
387
|
<DialogContent className="sm:max-w-100">
|
|
384
388
|
<DialogHeader>
|
|
385
|
-
<DialogTitle>{
|
|
389
|
+
<DialogTitle>{m['admin-users_dialog_password_title']({ username: user.username })}</DialogTitle>
|
|
386
390
|
</DialogHeader>
|
|
387
391
|
<form onSubmit={(e) => { e.preventDefault(); startTransition(async () => {
|
|
388
392
|
try {
|
|
389
393
|
await resetUserPasswordFn({ data: { userId: user.id, newPassword: passwordDialog.value } })
|
|
390
|
-
toast.success(
|
|
394
|
+
toast.success(m['admin-users_toast_passwordReset']())
|
|
391
395
|
setPasswordDialog((d) => ({ ...d, open: false, value: '' }))
|
|
392
|
-
} catch { toast.error(
|
|
396
|
+
} catch { toast.error(m['admin-users_toast_resetFailed']()) }
|
|
393
397
|
})}}>
|
|
394
398
|
<div className="py-4">
|
|
395
|
-
<Label>{
|
|
396
|
-
<Input type="password" required value={passwordDialog.value} onChange={(e) => setPasswordDialog((d) => ({ ...d, value: e.target.value }))} />
|
|
399
|
+
<Label>{m['admin-users_dialog_password_newPassword']()}</Label>
|
|
400
|
+
<Input type="password" required value={passwordDialog.value} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordDialog((d) => ({ ...d, value: e.target.value }))} />
|
|
397
401
|
</div>
|
|
398
402
|
<DialogFooter>
|
|
399
|
-
<Button type="button" variant="outline" onClick={() => setPasswordDialog((d) => ({ ...d, open: false }))}>{
|
|
400
|
-
<Button type="submit" disabled={isPending}>{
|
|
403
|
+
<Button type="button" variant="outline" onClick={() => setPasswordDialog((d) => ({ ...d, open: false }))}>{m.common_button_cancel()}</Button>
|
|
404
|
+
<Button type="submit" disabled={isPending}>{m['admin-users_dialog_password_confirm']()}</Button>
|
|
401
405
|
</DialogFooter>
|
|
402
406
|
</form>
|
|
403
407
|
</DialogContent>
|
|
404
408
|
</Dialog>
|
|
405
409
|
|
|
406
|
-
<AlertDialog open={deleteDialog.open} onOpenChange={(o) => setDeleteDialog((d) => ({ ...d, open: o }))}>
|
|
410
|
+
<AlertDialog open={deleteDialog.open} onOpenChange={(o: boolean) => setDeleteDialog((d) => ({ ...d, open: o }))}>
|
|
407
411
|
<AlertDialogContent>
|
|
408
412
|
<AlertDialogHeader>
|
|
409
|
-
<AlertDialogTitle>{
|
|
413
|
+
<AlertDialogTitle>{m['admin-users_dialog_delete_title']()}</AlertDialogTitle>
|
|
410
414
|
<AlertDialogDescription>
|
|
411
|
-
{
|
|
415
|
+
{m['admin-users_dialog_delete_description']({ username: user.username })}
|
|
412
416
|
</AlertDialogDescription>
|
|
413
417
|
</AlertDialogHeader>
|
|
414
|
-
<Input placeholder={
|
|
418
|
+
<Input placeholder={m['admin-users_dialog_delete_placeholder']()} value={deleteDialog.confirmName} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setDeleteDialog((d) => ({ ...d, confirmName: e.target.value }))} />
|
|
415
419
|
<AlertDialogFooter>
|
|
416
|
-
<AlertDialogCancel>{
|
|
420
|
+
<AlertDialogCancel>{m.common_button_cancel()}</AlertDialogCancel>
|
|
417
421
|
<AlertDialogAction
|
|
418
422
|
disabled={deleteDialog.confirmName !== user.username || isPending}
|
|
419
423
|
onClick={() => startTransition(async () => {
|
|
420
424
|
try {
|
|
421
425
|
await deleteUserFn({ data: { userId: user.id } })
|
|
422
|
-
toast.success(
|
|
426
|
+
toast.success(m['admin-users_toast_userDeleted']())
|
|
423
427
|
window.location.href = '/users'
|
|
424
|
-
} catch { toast.error(
|
|
428
|
+
} catch { toast.error(m['admin-users_toast_deleteFailed']()) }
|
|
425
429
|
})}
|
|
426
|
-
>{
|
|
430
|
+
>{m['admin-users_dialog_delete_confirm']()}</AlertDialogAction>
|
|
427
431
|
</AlertDialogFooter>
|
|
428
432
|
</AlertDialogContent>
|
|
429
433
|
</AlertDialog>
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { Outlet, createLazyFileRoute, useMatches } from '@tanstack/react-router'
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
19
20
|
import { useState } from 'react'
|
|
20
|
-
import { useTranslation } from 'react-i18next'
|
|
21
21
|
import { UsersTable } from './-components/users-table'
|
|
22
22
|
import { UserFilters } from '@/components/user/user-filters'
|
|
23
23
|
import { UserCreateDialog } from '@/components/user/user-create-dialog'
|
|
@@ -46,7 +46,6 @@ function UsersLayout() {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function UsersPage() {
|
|
49
|
-
const { t } = useTranslation('admin-users')
|
|
50
49
|
const { items, total } = Route.useLoaderData()
|
|
51
50
|
const { search, role, status } = Route.useSearch()
|
|
52
51
|
const [createOpen, setCreateOpen] = useState(false)
|
|
@@ -55,12 +54,12 @@ function UsersPage() {
|
|
|
55
54
|
<div className="space-y-4 p-6">
|
|
56
55
|
<div className="flex items-center justify-between">
|
|
57
56
|
<div>
|
|
58
|
-
<h2 className="text-xl font-bold">{
|
|
59
|
-
<p className="text-sm text-muted-foreground">{
|
|
57
|
+
<h2 className="text-xl font-bold">{m['admin-users_page_title']()}</h2>
|
|
58
|
+
<p className="text-sm text-muted-foreground">{m['admin-users_page_total']({ total })}</p>
|
|
60
59
|
</div>
|
|
61
60
|
<Button onClick={() => setCreateOpen(true)}>
|
|
62
61
|
<PlusIcon className="mr-1 h-4 w-4" />
|
|
63
|
-
{
|
|
62
|
+
{m['admin-users_page_createUser']()}
|
|
64
63
|
</Button>
|
|
65
64
|
</div>
|
|
66
65
|
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute, useNavigate } from '@tanstack/react-router'
|
|
19
|
+
import { m } from '@/paraglide/messages.js'
|
|
19
20
|
import { useState } from 'react'
|
|
20
|
-
import { useTranslation } from 'react-i18next'
|
|
21
21
|
import { toast } from '@/components/ui/sonner'
|
|
22
22
|
import { Loader2, ShieldCheck, User, Mail, Database } from 'lucide-react'
|
|
23
23
|
import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from '@/components/auth/auth-layout'
|
|
@@ -33,7 +33,6 @@ export const Route = createLazyFileRoute('/setup')({
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
function SetupPage() {
|
|
36
|
-
const { t } = useTranslation('common')
|
|
37
36
|
const navigate = useNavigate()
|
|
38
37
|
const [username, setUsername] = useState('')
|
|
39
38
|
const [email, setEmail] = useState('')
|
|
@@ -42,30 +41,30 @@ function SetupPage() {
|
|
|
42
41
|
const [loading, setLoading] = useState(false)
|
|
43
42
|
|
|
44
43
|
const setupBrandFeatures = [
|
|
45
|
-
{ icon: ShieldCheck, label:
|
|
46
|
-
{ icon: Database, label:
|
|
47
|
-
{ icon: User, label:
|
|
44
|
+
{ icon: ShieldCheck, label: m.common_setup_brandFeatures_superAdmin_label(), desc: m.common_setup_brandFeatures_superAdmin_desc() },
|
|
45
|
+
{ icon: Database, label: m.common_setup_brandFeatures_dataMgmt_label(), desc: m.common_setup_brandFeatures_dataMgmt_desc() },
|
|
46
|
+
{ icon: User, label: m.common_setup_brandFeatures_accountSecurity_label(), desc: m.common_setup_brandFeatures_accountSecurity_desc() },
|
|
48
47
|
]
|
|
49
48
|
|
|
50
49
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
51
50
|
e.preventDefault()
|
|
52
51
|
|
|
53
52
|
if (!username || !email || !password) {
|
|
54
|
-
toast.error(
|
|
53
|
+
toast.error(m.common_setup_validation_fillAllFields())
|
|
55
54
|
return
|
|
56
55
|
}
|
|
57
56
|
if (password !== confirmPassword) {
|
|
58
|
-
toast.error(
|
|
57
|
+
toast.error(m.common_setup_validation_passwordMismatch())
|
|
59
58
|
return
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
setLoading(true)
|
|
63
62
|
try {
|
|
64
63
|
await setupAdminFn({ data: { username, email, password } })
|
|
65
|
-
toast.success(
|
|
64
|
+
toast.success(m.common_setup_success())
|
|
66
65
|
setTimeout(() => navigate({ to: '/login' }), 500)
|
|
67
66
|
} catch (err) {
|
|
68
|
-
toast.error(err instanceof Error ? err.message :
|
|
67
|
+
toast.error(err instanceof Error ? err.message : m.common_setup_error())
|
|
69
68
|
} finally {
|
|
70
69
|
setLoading(false)
|
|
71
70
|
}
|
|
@@ -74,20 +73,20 @@ function SetupPage() {
|
|
|
74
73
|
return (
|
|
75
74
|
<AuthLayout>
|
|
76
75
|
<AuthLayoutLeft>
|
|
77
|
-
<AuthHeader title={
|
|
76
|
+
<AuthHeader title={m.common_setup_form_title()} subtitle={m.common_setup_form_subtitle()} />
|
|
78
77
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
79
|
-
<IconInput icon={User} label={
|
|
80
|
-
<IconInput icon={Mail} label={
|
|
81
|
-
<PasswordInput label={
|
|
82
|
-
<PasswordInput label={
|
|
78
|
+
<IconInput icon={User} label={m.common_setup_form_username_label()} placeholder={m.common_setup_form_username_placeholder()} value={username} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUsername(e.target.value)} />
|
|
79
|
+
<IconInput icon={Mail} label={m.common_setup_form_email_label()} placeholder={m.common_setup_form_email_placeholder()} value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} />
|
|
80
|
+
<PasswordInput label={m.common_setup_form_password_label()} placeholder={m.common_setup_form_password_placeholder()} value={password} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)} />
|
|
81
|
+
<PasswordInput label={m.common_setup_form_confirmPassword_label()} placeholder={m.common_setup_form_confirmPassword_placeholder()} value={confirmPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfirmPassword(e.target.value)} />
|
|
83
82
|
<Button type="submit" className="w-full" disabled={loading}>
|
|
84
83
|
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
85
|
-
{loading ?
|
|
84
|
+
{loading ? m.common_setup_form_loading() : m.common_setup_form_submit()}
|
|
86
85
|
</Button>
|
|
87
86
|
</form>
|
|
88
87
|
</AuthLayoutLeft>
|
|
89
88
|
<AuthLayoutRight>
|
|
90
|
-
<AuthBrandPanel title={
|
|
89
|
+
<AuthBrandPanel title={m.common_setup_brand_title()} subtitle={m.common_setup_brand_subtitle()} features={setupBrandFeatures} />
|
|
91
90
|
</AuthLayoutRight>
|
|
92
91
|
</AuthLayout>
|
|
93
92
|
)
|
package/src/start.ts
CHANGED
|
@@ -72,11 +72,11 @@ const apiCorsMiddleware = createMiddleware().server(async ({ request, next }: an
|
|
|
72
72
|
if (typeof process !== 'undefined' && process.versions?.node && process.env.NODE_ENV !== 'test') {
|
|
73
73
|
initErrorCollector()
|
|
74
74
|
|
|
75
|
-
getHealthCheckEnabled().then((enabled) => {
|
|
75
|
+
getHealthCheckEnabled().then((enabled: boolean) => {
|
|
76
76
|
if (enabled) {
|
|
77
77
|
startHealthCheckTimer(30_000)
|
|
78
78
|
}
|
|
79
|
-
}).catch((e) => console.error('[HealthCheckInit]', e))
|
|
79
|
+
}).catch((e: unknown) => console.error('[HealthCheckInit]', e))
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export const startInstance = createStart(() => ({
|
package/src/lib/i18n/client.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
}
|