@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.
- package/package.json +86 -0
- package/src/client.tsx +31 -0
- package/src/components/admin/admin-user-menu.tsx +90 -0
- package/src/components/admin/developer-log.tsx +356 -0
- package/src/components/ai-provider/model-list-editor.tsx +147 -0
- package/src/components/ai-provider/provider-form.tsx +434 -0
- package/src/components/ai-provider/provider-icon.tsx +43 -0
- package/src/components/ai-provider/provider-logo.tsx +87 -0
- package/src/components/ai-provider/provider-table.tsx +258 -0
- package/src/components/auth/admin-login-form.tsx +214 -0
- package/src/components/auth/auth-brand-panel.tsx +60 -0
- package/src/components/auth/auth-header.tsx +38 -0
- package/src/components/auth/auth-layout.tsx +66 -0
- package/src/components/auth/auth-providers.tsx +34 -0
- package/src/components/auth/icon-input.tsx +46 -0
- package/src/components/auth/oauth-section.tsx +51 -0
- package/src/components/auth/password-input.tsx +67 -0
- package/src/components/common/page-header.tsx +42 -0
- package/src/components/common/placeholder-page.tsx +42 -0
- package/src/components/component/component-form-dialog.tsx +348 -0
- package/src/components/component/component-import-dialog.tsx +235 -0
- package/src/components/component/component-table.tsx +153 -0
- package/src/components/data-table/data-table-pagination.tsx +114 -0
- package/src/components/data-table/data-table.tsx +218 -0
- package/src/components/data-table/table-detail-layout.tsx +99 -0
- package/src/components/dependency/changelog-dialog.tsx +108 -0
- package/src/components/dependency/columns.tsx +232 -0
- package/src/components/dependency/dependency-table.tsx +368 -0
- package/src/components/dependency/edit-dialog.tsx +200 -0
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
- package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
- package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
- package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
- package/src/components/layout/sidebar-02/index.tsx +33 -0
- package/src/components/layout/sidebar-02/logo.tsx +40 -0
- package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
- package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
- package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
- package/src/components/magicui/animated-theme-toggler.tsx +272 -0
- package/src/components/magicui/rainbow-button.tsx +78 -0
- package/src/components/market/columns.tsx +151 -0
- package/src/components/market/market-app-form.tsx +237 -0
- package/src/components/market/market-app-list.tsx +156 -0
- package/src/components/monitoring/error-list.tsx +143 -0
- package/src/components/monitoring/health-cards.tsx +152 -0
- package/src/components/monitoring/metrics-stats.tsx +184 -0
- package/src/components/monitoring/request-log-table.tsx +179 -0
- package/src/components/providers/theme-toggle.tsx +59 -0
- package/src/components/settings/avatar/avatar-config.ts +46 -0
- package/src/components/ui/alert-dialog.tsx +216 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/avatar.tsx +129 -0
- package/src/components/ui/badge.tsx +66 -0
- package/src/components/ui/button.tsx +84 -0
- package/src/components/ui/card.tsx +120 -0
- package/src/components/ui/checkbox.tsx +50 -0
- package/src/components/ui/collapsible.tsx +50 -0
- package/src/components/ui/dialog.tsx +185 -0
- package/src/components/ui/dropdown-menu.tsx +286 -0
- package/src/components/ui/field.tsx +255 -0
- package/src/components/ui/input-group.tsx +171 -0
- package/src/components/ui/input.tsx +36 -0
- package/src/components/ui/label.tsx +41 -0
- package/src/components/ui/pagination.tsx +146 -0
- package/src/components/ui/select.tsx +209 -0
- package/src/components/ui/separator.tsx +45 -0
- package/src/components/ui/sheet.tsx +164 -0
- package/src/components/ui/sidebar.tsx +719 -0
- package/src/components/ui/skeleton.tsx +30 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +48 -0
- package/src/components/ui/table.tsx +133 -0
- package/src/components/ui/textarea.tsx +35 -0
- package/src/components/ui/tooltip.tsx +74 -0
- package/src/components/user/columns.tsx +153 -0
- package/src/components/user/settings-activity-log.tsx +358 -0
- package/src/components/user/settings-profile/constants.ts +42 -0
- package/src/components/user/settings-profile/types.ts +47 -0
- package/src/components/user/settings-profile.tsx +664 -0
- package/src/components/user/user-create-dialog.tsx +137 -0
- package/src/components/user/user-filters.tsx +117 -0
- package/src/hooks/use-mobile.ts +36 -0
- package/src/instrument.client.ts +34 -0
- package/src/lib/anyhub/capabilities.ts +59 -0
- package/src/lib/anyhub/types.ts +43 -0
- package/src/lib/auth-client.ts +83 -0
- package/src/lib/auth-utils.ts +24 -0
- package/src/lib/auth.config.ts +264 -0
- package/src/lib/avatar.ts +41 -0
- package/src/lib/i18n/client.ts +32 -0
- package/src/lib/i18n/config.ts +40 -0
- package/src/lib/i18n/detect.ts +43 -0
- package/src/lib/i18n/locales/en/admin-ai.json +128 -0
- package/src/lib/i18n/locales/en/admin-billing.json +56 -0
- package/src/lib/i18n/locales/en/admin-users.json +88 -0
- package/src/lib/i18n/locales/en/common.json +877 -0
- package/src/lib/i18n/locales/en/enterprise.json +208 -0
- package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
- package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
- package/src/lib/i18n/locales/zh/admin-users.json +88 -0
- package/src/lib/i18n/locales/zh/common.json +877 -0
- package/src/lib/i18n/locales/zh/enterprise.json +208 -0
- package/src/lib/i18n/server.ts +18 -0
- package/src/lib/i18n/types.ts +5 -0
- package/src/lib/monitoring-benchmarks.ts +85 -0
- package/src/lib/monitoring-descriptions.ts +78 -0
- package/src/lib/prisma.ts +18 -0
- package/src/lib/router-context.ts +38 -0
- package/src/lib/session.ts +38 -0
- package/src/lib/utils.ts +23 -0
- package/src/providers/index.tsx +45 -0
- package/src/routeTree.gen.ts +1199 -0
- package/src/router.tsx +51 -0
- package/src/routes/__root.tsx +170 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
- package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
- package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
- package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
- package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
- package/src/routes/_nav/ai-providers/route.tsx +31 -0
- package/src/routes/_nav/apps/route.lazy.tsx +30 -0
- package/src/routes/_nav/apps/route.tsx +20 -0
- package/src/routes/_nav/audit/route.lazy.tsx +100 -0
- package/src/routes/_nav/audit/route.tsx +25 -0
- package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
- package/src/routes/_nav/billing/invoices/route.tsx +25 -0
- package/src/routes/_nav/billing/route.lazy.tsx +48 -0
- package/src/routes/_nav/billing/route.tsx +27 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
- package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
- package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
- package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
- package/src/routes/_nav/compliance/route.tsx +25 -0
- package/src/routes/_nav/components/route.lazy.tsx +184 -0
- package/src/routes/_nav/components/route.tsx +40 -0
- package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
- package/src/routes/_nav/dashboard/route.tsx +20 -0
- package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
- package/src/routes/_nav/dependencies/route.tsx +32 -0
- package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
- package/src/routes/_nav/developer-logs/route.tsx +14 -0
- package/src/routes/_nav/docs/route.lazy.tsx +30 -0
- package/src/routes/_nav/docs/route.tsx +20 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
- package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
- package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
- package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
- package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
- package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
- package/src/routes/_nav/licenses/route.tsx +25 -0
- package/src/routes/_nav/market/route.lazy.tsx +31 -0
- package/src/routes/_nav/market/route.tsx +20 -0
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
- package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
- package/src/routes/_nav/monitoring/route.tsx +20 -0
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
- package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
- package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
- package/src/routes/_nav/permissions/route.tsx +26 -0
- package/src/routes/_nav/route.lazy.tsx +31 -0
- package/src/routes/_nav/route.tsx +34 -0
- package/src/routes/_nav/settings/route.lazy.tsx +30 -0
- package/src/routes/_nav/settings/route.tsx +20 -0
- package/src/routes/_nav/sso/route.lazy.tsx +145 -0
- package/src/routes/_nav/sso/route.tsx +25 -0
- package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
- package/src/routes/_nav/users/$userId/route.tsx +27 -0
- package/src/routes/_nav/users/-components/users-table.tsx +38 -0
- package/src/routes/_nav/users/route.lazy.tsx +74 -0
- package/src/routes/_nav/users/route.tsx +43 -0
- package/src/routes/api/compliance-download/$jobId.ts +35 -0
- package/src/routes/api/observability/errors.ts +41 -0
- package/src/routes/api/observability/health.ts +30 -0
- package/src/routes/api.auth.$.ts +31 -0
- package/src/routes/api.sentry-test.ts +33 -0
- package/src/routes/index/route.lazy.tsx +22 -0
- package/src/routes/index/route.tsx +24 -0
- package/src/routes/login/route.lazy.tsx +46 -0
- package/src/routes/login/route.tsx +28 -0
- package/src/routes/sentry-tunnel.ts +61 -0
- package/src/routes/setup/route.lazy.tsx +94 -0
- package/src/routes/setup/route.tsx +28 -0
- package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
- package/src/server/__tests__/licenses-issue.test.ts +98 -0
- package/src/server/admin-ai-alerts.ts +177 -0
- package/src/server/admin-ai-usage.ts +134 -0
- package/src/server/auth-helpers.ts +51 -0
- package/src/server/entitlements.ts +24 -0
- package/src/server/knowledge-public/canonical-id.ts +55 -0
- package/src/server/knowledge-public/github-sync.ts +86 -0
- package/src/server/license-issue.ts +78 -0
- package/src/server/middlewares/request.ts +26 -0
- package/src/server/observability/debug-middleware.ts +39 -0
- package/src/server/observability/error-reporter.ts +99 -0
- package/src/server/observability/health-check.ts +140 -0
- package/src/server/observability/index.ts +33 -0
- package/src/server/observability/logger.ts +49 -0
- package/src/server/observability/metrics.ts +105 -0
- package/src/server/observability/request-middleware.ts +125 -0
- package/src/server/observability/timing.ts +42 -0
- package/src/server/pro-services.ts +32 -0
- package/src/server/serverFns/admin-billing.ts +94 -0
- package/src/server/serverFns/ai-provider/create.ts +92 -0
- package/src/server/serverFns/ai-provider/delete.ts +42 -0
- package/src/server/serverFns/ai-provider/get.ts +35 -0
- package/src/server/serverFns/ai-provider/list.ts +92 -0
- package/src/server/serverFns/ai-provider/update.ts +73 -0
- package/src/server/serverFns/anyhub/market.ts +64 -0
- package/src/server/serverFns/audit.ts +57 -0
- package/src/server/serverFns/auth/session.ts +26 -0
- package/src/server/serverFns/auth/setup.ts +73 -0
- package/src/server/serverFns/compliance.ts +85 -0
- package/src/server/serverFns/components/import.ts +197 -0
- package/src/server/serverFns/components/list.ts +51 -0
- package/src/server/serverFns/components/mutations.ts +84 -0
- package/src/server/serverFns/dependency/helpers.ts +245 -0
- package/src/server/serverFns/dependency/list.ts +127 -0
- package/src/server/serverFns/dependency/sync.ts +123 -0
- package/src/server/serverFns/dependency/update.ts +51 -0
- package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
- package/src/server/serverFns/developer-logs.ts +132 -0
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
- package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
- package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
- package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
- package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
- package/src/server/serverFns/licenses.ts +92 -0
- package/src/server/serverFns/locale.ts +10 -0
- package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
- package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
- package/src/server/serverFns/monitoring/get-errors.ts +43 -0
- package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
- package/src/server/serverFns/monitoring/get-health.ts +55 -0
- package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
- package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
- package/src/server/serverFns/monitoring/report-error.ts +55 -0
- package/src/server/serverFns/monitoring/reset.ts +70 -0
- package/src/server/serverFns/permissions.ts +214 -0
- package/src/server/serverFns/sentry/config.ts +88 -0
- package/src/server/serverFns/sso-config.ts +83 -0
- package/src/server/serverFns/user/create.ts +40 -0
- package/src/server/serverFns/user/delete.ts +29 -0
- package/src/server/serverFns/user/detail.ts +45 -0
- package/src/server/serverFns/user/list.ts +56 -0
- package/src/server/serverFns/user/reset-password.ts +28 -0
- package/src/server/serverFns/user/update-role.ts +26 -0
- package/src/server/serverFns/user/update-status.ts +26 -0
- package/src/server.ts +27 -0
- package/src/start.ts +85 -0
- package/src/styles/_keyframe-animations.scss +91 -0
- package/src/styles/_variables.scss +296 -0
- package/src/styles/globals.css +200 -0
- 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')({})
|