@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,264 @@
|
|
|
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 type { StartAuthJSConfig } from 'start-authjs'
|
|
19
|
+
import type { SessionUser } from '@/lib/session'
|
|
20
|
+
import CredentialsProvider from '@auth/core/providers/credentials'
|
|
21
|
+
import type { PrismaClient } from '@allbluecn/database'
|
|
22
|
+
import bcrypt from 'bcryptjs'
|
|
23
|
+
import { getRandomConfig, presetBgColors } from '@/components/settings/avatar/avatar-config'
|
|
24
|
+
import { resolveIpLocation, getClientIp } from '@allbluecn/kernel'
|
|
25
|
+
|
|
26
|
+
let _prisma: PrismaClient | null = null
|
|
27
|
+
async function getDb(): Promise<PrismaClient> {
|
|
28
|
+
if (!_prisma) {
|
|
29
|
+
const mod = await import('@allbluecn/database')
|
|
30
|
+
_prisma = mod.prisma
|
|
31
|
+
}
|
|
32
|
+
return _prisma
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pickBgColor(seed: string) {
|
|
36
|
+
let hash = 0
|
|
37
|
+
for (let i = 0; i < seed.length; i++) hash = seed.charCodeAt(i) + ((hash << 5) - hash)
|
|
38
|
+
return presetBgColors[Math.abs(hash) % presetBgColors.length]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const authConfig: StartAuthJSConfig = {
|
|
42
|
+
session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60 },
|
|
43
|
+
pages: { signIn: '/login', error: '/login' },
|
|
44
|
+
secret: process.env.AUTH_SECRET,
|
|
45
|
+
trustHost: true,
|
|
46
|
+
cookies: {
|
|
47
|
+
sessionToken: {
|
|
48
|
+
name: 'auth.session-token',
|
|
49
|
+
options: {
|
|
50
|
+
httpOnly: true,
|
|
51
|
+
sameSite: 'lax',
|
|
52
|
+
path: '/',
|
|
53
|
+
secure: process.env.NODE_ENV === 'production',
|
|
54
|
+
domain: process.env.COOKIE_DOMAIN || undefined,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
providers: [
|
|
59
|
+
CredentialsProvider({
|
|
60
|
+
name: 'credentials',
|
|
61
|
+
credentials: {
|
|
62
|
+
email: { label: 'Email', type: 'email' },
|
|
63
|
+
password: { label: 'Password', type: 'password' },
|
|
64
|
+
},
|
|
65
|
+
async authorize(credentials, request) {
|
|
66
|
+
const creds = credentials as { email: string; password: string; remember?: string; stayLoggedIn?: string }
|
|
67
|
+
if (!creds.email || !creds.password) return null
|
|
68
|
+
|
|
69
|
+
const user = await getDb().then(db => db.user.findUnique({
|
|
70
|
+
where: { email: creds.email },
|
|
71
|
+
select: { id: true, email: true, username: true, password: true, role: true, status: true },
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
// 用户不存在:userId 为外键,无法记录日志(保持现状)
|
|
75
|
+
if (!user) return null
|
|
76
|
+
|
|
77
|
+
// 安全提取 IP/UA(start-authjs 传递的 request 类型可能不标准,失败不阻塞登录)
|
|
78
|
+
let ip: string | null = null
|
|
79
|
+
let userAgent: string | null = null
|
|
80
|
+
try {
|
|
81
|
+
const headers = request?.headers as { get?: (name: string) => string | null } | undefined
|
|
82
|
+
if (headers && typeof headers.get === 'function') {
|
|
83
|
+
ip = getClientIp(request as Request)
|
|
84
|
+
userAgent = headers.get('user-agent') || null
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// 忽略:IP/UA 仅用于日志,获取失败不影响登录
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 安全记录登录日志(DB 异常不阻塞登录流程)
|
|
91
|
+
const recordLog = async (success: boolean, message?: string, location?: string) => {
|
|
92
|
+
try {
|
|
93
|
+
await getDb().then(db => db.loginLog.create({
|
|
94
|
+
data: {
|
|
95
|
+
userId: user.id,
|
|
96
|
+
ip,
|
|
97
|
+
userAgent,
|
|
98
|
+
location: location ?? null,
|
|
99
|
+
success,
|
|
100
|
+
...(message ? { message } : {}),
|
|
101
|
+
},
|
|
102
|
+
}))
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('[auth] 记录登录日志失败:', err)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 账号状态异常:记录失败原因
|
|
109
|
+
if (user.status === 'BANNED') {
|
|
110
|
+
await recordLog(false, '账号已封禁')
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
if (user.status === 'INACTIVE') {
|
|
114
|
+
await recordLog(false, '账号未激活')
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
if (user.status !== 'ACTIVE') {
|
|
118
|
+
await recordLog(false, '账号状态异常')
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 非管理员:admin 端专用拦截
|
|
123
|
+
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
|
124
|
+
await recordLog(false, '非管理员账号')
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 密码校验
|
|
129
|
+
const isValid = await bcrypt.compare(creds.password, user.password)
|
|
130
|
+
if (!isValid) {
|
|
131
|
+
await recordLog(false, '密码错误')
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 登录成功:记录 IP/UA/地理位置
|
|
136
|
+
const location = await resolveIpLocation(ip)
|
|
137
|
+
await recordLog(true, undefined, location)
|
|
138
|
+
await getDb().then(db => db.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } }))
|
|
139
|
+
|
|
140
|
+
const remember = creds.remember === 'true'
|
|
141
|
+
const stayLoggedIn = creds.stayLoggedIn === 'true'
|
|
142
|
+
return {
|
|
143
|
+
id: user.id,
|
|
144
|
+
email: user.email,
|
|
145
|
+
name: user.username,
|
|
146
|
+
role: user.role,
|
|
147
|
+
remember,
|
|
148
|
+
stayLoggedIn,
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
],
|
|
153
|
+
callbacks: {
|
|
154
|
+
async jwt({ token, user, trigger }) {
|
|
155
|
+
// DB 查询失败(Prisma Client 未 generate / 字段缺失 / 连接异常)不应炸 jwt 流程
|
|
156
|
+
try {
|
|
157
|
+
if (user) {
|
|
158
|
+
token.role = (user as { role: string }).role
|
|
159
|
+
token.id = user.id
|
|
160
|
+
const dbUser = await getDb().then(db => db.user.findUnique({
|
|
161
|
+
where: { id: user.id },
|
|
162
|
+
select: { avatarConfig: true, bgShape: true, bgColor: true, username: true },
|
|
163
|
+
}))
|
|
164
|
+
if (dbUser) {
|
|
165
|
+
if (dbUser.avatarConfig) {
|
|
166
|
+
token.avatarConfig = dbUser.avatarConfig as Record<string, number>
|
|
167
|
+
token.bgShape = dbUser.bgShape ?? 'square'
|
|
168
|
+
token.bgColor = dbUser.bgColor ?? '#fff'
|
|
169
|
+
} else {
|
|
170
|
+
const avatarConfig = getRandomConfig()
|
|
171
|
+
const bgColor = pickBgColor(dbUser.username)
|
|
172
|
+
await getDb().then(db => db.user.update({
|
|
173
|
+
where: { id: user.id },
|
|
174
|
+
data: { avatarConfig, bgShape: 'square', bgColor },
|
|
175
|
+
}))
|
|
176
|
+
token.avatarConfig = avatarConfig as Record<string, number>
|
|
177
|
+
token.bgShape = 'square'
|
|
178
|
+
token.bgColor = bgColor
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
token.lastActivity = Date.now()
|
|
182
|
+
token.adminTimeout = (user as { stayLoggedIn?: boolean }).stayLoggedIn
|
|
183
|
+
? 12 * 60 * 60 * 1000
|
|
184
|
+
: 30 * 60 * 1000
|
|
185
|
+
}
|
|
186
|
+
if (token.id && trigger === 'update') {
|
|
187
|
+
const dbUser = await getDb().then(db => db.user.findUnique({
|
|
188
|
+
where: { id: token.id as string },
|
|
189
|
+
select: { avatarConfig: true, bgShape: true, bgColor: true, username: true },
|
|
190
|
+
}))
|
|
191
|
+
if (dbUser) {
|
|
192
|
+
token.avatarConfig = (dbUser.avatarConfig ?? getRandomConfig()) as Record<string, number>
|
|
193
|
+
token.bgShape = dbUser.bgShape ?? 'square'
|
|
194
|
+
token.bgColor = dbUser.bgColor ?? pickBgColor(dbUser.username)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (token.id && !user && trigger !== 'update') {
|
|
198
|
+
const dbUser = await getDb().then(db => db.user.findUnique({
|
|
199
|
+
where: { id: token.id as string },
|
|
200
|
+
select: { bgShape: true, bgColor: true, avatarConfig: true, username: true },
|
|
201
|
+
}))
|
|
202
|
+
if (dbUser) {
|
|
203
|
+
token.bgShape = dbUser.bgShape ?? 'square'
|
|
204
|
+
token.bgColor = dbUser.bgColor ?? pickBgColor(dbUser.username)
|
|
205
|
+
token.avatarConfig = (dbUser.avatarConfig ?? getRandomConfig()) as Record<string, number>
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (token.lastActivity && token.adminTimeout) {
|
|
209
|
+
token.lastActivity = Date.now()
|
|
210
|
+
}
|
|
211
|
+
return token
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error('[auth][admin] jwt callback 查询失败,降级返回原 token:', e)
|
|
214
|
+
return token
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
async session({ session, token }) {
|
|
218
|
+
if (session.user) {
|
|
219
|
+
if (token.lastActivity && token.adminTimeout) {
|
|
220
|
+
const elapsed = Date.now() - (token.lastActivity as number)
|
|
221
|
+
if (elapsed > (token.adminTimeout as number)) {
|
|
222
|
+
return { ...session, user: null } as unknown as typeof session
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 用户存在性校验:token 指向的用户在 DB 不存在(DB 重置 / 用户被删 + 旧 JWT 残留)
|
|
226
|
+
// DB 查询失败时降级跳过(不返回 user:null 导致登出),避免 Prisma Client 问题连锁影响登录
|
|
227
|
+
if (token.id) {
|
|
228
|
+
try {
|
|
229
|
+
const exists = await getDb().then(db => db.user.findUnique({
|
|
230
|
+
where: { id: token.id as string },
|
|
231
|
+
select: { id: true },
|
|
232
|
+
}))
|
|
233
|
+
if (!exists) {
|
|
234
|
+
return { ...session, user: null } as unknown as typeof session
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.error('[auth][admin] session callback 用户存在性检查失败,跳过:', e)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const user = session.user as unknown as SessionUser
|
|
241
|
+
user.role = token.role as string
|
|
242
|
+
user.id = token.id as string
|
|
243
|
+
user.avatarConfig = token.avatarConfig as Record<string, number> | undefined
|
|
244
|
+
user.bgShape = token.bgShape as string | undefined
|
|
245
|
+
user.bgColor = token.bgColor as string | undefined
|
|
246
|
+
if (token.name) user.name = token.name as string | null
|
|
247
|
+
if (token.email) user.email = token.email as string | null
|
|
248
|
+
user.image = (token.picture as string | null) ?? null
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const sub = await getDb().then(db => db.subscription.findUnique({
|
|
252
|
+
where: { userId: token.id as string },
|
|
253
|
+
select: { status: true, plan: { select: { name: true } } },
|
|
254
|
+
}))
|
|
255
|
+
user.planLabel =
|
|
256
|
+
sub?.status === 'ACTIVE' && sub.plan.name === 'pro' ? 'pro' : 'free'
|
|
257
|
+
} catch {
|
|
258
|
+
user.planLabel = 'free'
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return session
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
const DICE_BEAR_BASE = "https://api.dicebear.com/7.x";
|
|
19
|
+
|
|
20
|
+
const DICE_BEAR_STYLES = ["avataaars", "micah", "notionists", "lorelei", "adventurer"] as const;
|
|
21
|
+
|
|
22
|
+
type DiceBearStyle = (typeof DICE_BEAR_STYLES)[number];
|
|
23
|
+
|
|
24
|
+
export interface AvatarOptions {
|
|
25
|
+
style?: DiceBearStyle;
|
|
26
|
+
size?: number;
|
|
27
|
+
square?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getBoringAvatarUrl(
|
|
31
|
+
seed: string,
|
|
32
|
+
options: AvatarOptions = {}
|
|
33
|
+
): string {
|
|
34
|
+
const { style = "avataaars", size = 80 } = options;
|
|
35
|
+
|
|
36
|
+
return `${DICE_BEAR_BASE}/${style}/svg?seed=${encodeURIComponent(seed)}&size=${size}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getAvatarFallback(name: string): string {
|
|
40
|
+
return name.charAt(0).toUpperCase();
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import zhCommon from './locales/zh/common.json'
|
|
3
|
+
import enCommon from './locales/en/common.json'
|
|
4
|
+
import zhUsers from './locales/zh/admin-users.json'
|
|
5
|
+
import enUsers from './locales/en/admin-users.json'
|
|
6
|
+
import zhAi from './locales/zh/admin-ai.json'
|
|
7
|
+
import enAi from './locales/en/admin-ai.json'
|
|
8
|
+
import zhBilling from './locales/zh/admin-billing.json'
|
|
9
|
+
import enBilling from './locales/en/admin-billing.json'
|
|
10
|
+
import zhEnterprise from './locales/zh/enterprise.json'
|
|
11
|
+
import enEnterprise from './locales/en/enterprise.json'
|
|
12
|
+
|
|
13
|
+
export const supportedLngs = ['zh', 'en'] as const
|
|
14
|
+
export type AppLocale = (typeof supportedLngs)[number]
|
|
15
|
+
export const fallbackLng: AppLocale = 'zh'
|
|
16
|
+
export const defaultNs = 'common'
|
|
17
|
+
|
|
18
|
+
export const resources = {
|
|
19
|
+
zh: {
|
|
20
|
+
common: zhCommon,
|
|
21
|
+
'admin-users': zhUsers,
|
|
22
|
+
'admin-ai': zhAi,
|
|
23
|
+
'admin-billing': zhBilling,
|
|
24
|
+
enterprise: zhEnterprise,
|
|
25
|
+
},
|
|
26
|
+
en: {
|
|
27
|
+
common: enCommon,
|
|
28
|
+
'admin-users': enUsers,
|
|
29
|
+
'admin-ai': enAi,
|
|
30
|
+
'admin-billing': enBilling,
|
|
31
|
+
enterprise: enEnterprise,
|
|
32
|
+
},
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
export const namespaces = Object.keys(resources.zh) as Array<keyof typeof resources.zh>
|
|
36
|
+
export type NamespaceKey = (typeof namespaces)[number]
|
|
37
|
+
|
|
38
|
+
export function isSupportedLocale(lng: string): lng is AppLocale {
|
|
39
|
+
return (supportedLngs as readonly string[]).includes(lng)
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { supportedLngs, fallbackLng, isSupportedLocale, type AppLocale } from './config'
|
|
3
|
+
|
|
4
|
+
export function detectLocale(request: Request, userLocale: string | null): AppLocale {
|
|
5
|
+
const url = new URL(request.url)
|
|
6
|
+
const queryLng = url.searchParams.get('lng')
|
|
7
|
+
if (queryLng && isSupportedLocale(queryLng)) return queryLng
|
|
8
|
+
|
|
9
|
+
const cookieHeader = request.headers.get('cookie') ?? ''
|
|
10
|
+
const cookieLng = parseCookieValue(cookieHeader, 'lng')
|
|
11
|
+
if (cookieLng && isSupportedLocale(cookieLng)) return cookieLng
|
|
12
|
+
|
|
13
|
+
if (userLocale && isSupportedLocale(userLocale)) return userLocale
|
|
14
|
+
|
|
15
|
+
const acceptLang = request.headers.get('accept-language')
|
|
16
|
+
if (acceptLang) {
|
|
17
|
+
const detected = matchBestLanguage(acceptLang)
|
|
18
|
+
if (detected) return detected
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return fallbackLng
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseCookieValue(cookieHeader: string, key: string): string | null {
|
|
25
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${key}=([^;]+)`))
|
|
26
|
+
return match ? match[1] : null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchBestLanguage(acceptLang: string): AppLocale | null {
|
|
30
|
+
const entries = acceptLang
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((part) => {
|
|
33
|
+
const [lang, qPart] = part.trim().split(';q=')
|
|
34
|
+
return { lang: lang.trim(), q: qPart ? parseFloat(qPart) : 1 }
|
|
35
|
+
})
|
|
36
|
+
.sort((a, b) => b.q - a.q)
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const base = entry.lang.split('-')[0].toLowerCase()
|
|
40
|
+
if (isSupportedLocale(base)) return base as AppLocale
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"page": {
|
|
3
|
+
"title": "AI Providers",
|
|
4
|
+
"description": "Manage AI provider configurations, models, and limits",
|
|
5
|
+
"create": "Create Provider",
|
|
6
|
+
"edit": "Edit Provider"
|
|
7
|
+
},
|
|
8
|
+
"provider": {
|
|
9
|
+
"name": "Provider",
|
|
10
|
+
"kind": "Adapter Type",
|
|
11
|
+
"status": "Status",
|
|
12
|
+
"apiKey": "API Key",
|
|
13
|
+
"baseUrl": "Base URL",
|
|
14
|
+
"models": "Models",
|
|
15
|
+
"enabled": "Enabled",
|
|
16
|
+
"disabled": "Disabled",
|
|
17
|
+
"totalKeys": "Total Keys",
|
|
18
|
+
"monthlyUsage": "Monthly Usage",
|
|
19
|
+
"displayName": "Display Name",
|
|
20
|
+
"brandName": "Brand Name",
|
|
21
|
+
"description": "Description",
|
|
22
|
+
"category": "Category",
|
|
23
|
+
"defaultBaseUrl": "Default Base URL",
|
|
24
|
+
"keyUrl": "Key URL",
|
|
25
|
+
"docsUrl": "Docs URL",
|
|
26
|
+
"keyPlaceholder": "Key Placeholder",
|
|
27
|
+
"iconSlug": "Icon Slug",
|
|
28
|
+
"brandColor": "Brand Color",
|
|
29
|
+
"isLocal": "Local Provider",
|
|
30
|
+
"sortOrder": "Sort Order"
|
|
31
|
+
},
|
|
32
|
+
"action": {
|
|
33
|
+
"addProvider": "Add Provider",
|
|
34
|
+
"editProvider": "Edit Provider",
|
|
35
|
+
"testConnection": "Test Connection",
|
|
36
|
+
"manageModels": "Manage Models",
|
|
37
|
+
"toggleEnabled": "Toggle Enabled"
|
|
38
|
+
},
|
|
39
|
+
"model": {
|
|
40
|
+
"id": "Model ID",
|
|
41
|
+
"name": "Model Name",
|
|
42
|
+
"inputPrice": "Input Price",
|
|
43
|
+
"outputPrice": "Output Price",
|
|
44
|
+
"capabilities": "Capabilities",
|
|
45
|
+
"custom": "Custom",
|
|
46
|
+
"apiFetched": "API Fetched"
|
|
47
|
+
},
|
|
48
|
+
"form": {
|
|
49
|
+
"addProvider": "Add AI Provider",
|
|
50
|
+
"editProvider": "Edit AI Provider",
|
|
51
|
+
"basicInfo": "Basic Info",
|
|
52
|
+
"integrationConfig": "Integration Config",
|
|
53
|
+
"modelList": "Models",
|
|
54
|
+
"providerId": "Provider ID",
|
|
55
|
+
"providerIdDesc": "Business identifier, lowercase letters/numbers/dashes only, used for adapter routing.",
|
|
56
|
+
"providerIdEditNote": "Cannot be changed after creation",
|
|
57
|
+
"providerIdPlaceholder": "e.g. openai, deepseek",
|
|
58
|
+
"preview": "Preview",
|
|
59
|
+
"previewDesc": "Icon preview appears after filling in",
|
|
60
|
+
"displayName": "Display Name",
|
|
61
|
+
"displayNameDesc": "Chinese name shown on user-side card",
|
|
62
|
+
"brandName": "Brand Name",
|
|
63
|
+
"brandNameDesc": "English brand name (used for first-letter fallback)",
|
|
64
|
+
"description": "Description",
|
|
65
|
+
"descriptionDesc": "One-line provider description",
|
|
66
|
+
"category": "Category",
|
|
67
|
+
"adapterKind": "Adapter Type",
|
|
68
|
+
"adapterKindDesc": "Determines which SDK protocol to call",
|
|
69
|
+
"badge": "Badge Text",
|
|
70
|
+
"badgeDesc": "Label shown in top-right of card (e.g. 「Recommended」「Hot」「NEW」), leave empty to hide",
|
|
71
|
+
"badgePlaceholder": "e.g. Recommended",
|
|
72
|
+
"brandColor": "Brand Color",
|
|
73
|
+
"brandColorDesc": "Card accent color (hex)",
|
|
74
|
+
"iconSlug": "Icon Slug",
|
|
75
|
+
"iconSlugDesc": "Slug from @lobehub/icons (e.g. OpenAI, Anthropic), leave empty for first-letter fallback",
|
|
76
|
+
"iconSlugPlaceholder": "e.g. openai",
|
|
77
|
+
"defaultBaseUrl": "Default BaseURL",
|
|
78
|
+
"defaultBaseUrlDesc": "Used when user doesn't specify a custom endpoint",
|
|
79
|
+
"keyUrl": "API Key URL",
|
|
80
|
+
"docsUrl": "Docs URL",
|
|
81
|
+
"apikeyPlaceholder": "API Key Placeholder",
|
|
82
|
+
"localDeploy": "Local Deploy",
|
|
83
|
+
"localDeployDesc": "Check for local services like Ollama; API Key not required",
|
|
84
|
+
"platformEnabled": "Platform Enabled",
|
|
85
|
+
"platformEnabledDesc": "Disabled providers are hidden from unconfigured users; configured users keep access",
|
|
86
|
+
"sortOrder": "Sort Weight",
|
|
87
|
+
"sortOrderDesc": "Lower numbers appear first",
|
|
88
|
+
"modelListDesc": "Users select enabled models from this list (optional, can leave empty)",
|
|
89
|
+
"validation": {
|
|
90
|
+
"providerIdRequired": "Please fill in Provider ID",
|
|
91
|
+
"displayNameRequired": "Please fill in Display Name"
|
|
92
|
+
},
|
|
93
|
+
"toast": {
|
|
94
|
+
"created": "Provider created",
|
|
95
|
+
"updated": "Provider updated",
|
|
96
|
+
"saveFailed": "Save failed: {{error}}"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"table": {
|
|
100
|
+
"pageTitle": "AI Provider Management",
|
|
101
|
+
"pageDescription": "Manage {{count}} providers. Disabled providers are hidden from unconfigured users; configured users keep access. Unused providers can be hard-deleted.",
|
|
102
|
+
"addProvider": "Add Provider",
|
|
103
|
+
"columns": {
|
|
104
|
+
"provider": "Provider",
|
|
105
|
+
"adapterKind": "Model Adapter",
|
|
106
|
+
"category": "Category",
|
|
107
|
+
"badge": "Badge",
|
|
108
|
+
"modelCount": "Model Count",
|
|
109
|
+
"userConfigCount": "User Configs",
|
|
110
|
+
"platformEnabled": "Platform Enabled",
|
|
111
|
+
"actions": "Actions"
|
|
112
|
+
},
|
|
113
|
+
"enabledToggle": "Platform Enabled Toggle",
|
|
114
|
+
"edit": "Edit",
|
|
115
|
+
"delete": "Delete",
|
|
116
|
+
"deleteDisabled": "{{count}} users have configured this, cannot delete",
|
|
117
|
+
"deleteConfirm": {
|
|
118
|
+
"title": "Confirm delete provider 「{{name}}」?",
|
|
119
|
+
"description": "This action cannot be undone. The provider will be permanently removed. If you only want to temporarily disable, use the 「Disable」 function instead.",
|
|
120
|
+
"confirm": "Confirm Delete"
|
|
121
|
+
},
|
|
122
|
+
"toast": {
|
|
123
|
+
"updated": "Updated",
|
|
124
|
+
"deleted": "Provider deleted",
|
|
125
|
+
"deleteFailed": "Delete failed"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"page": {
|
|
3
|
+
"title": "Billing",
|
|
4
|
+
"description": "Manage subscriptions, invoices, and revenue",
|
|
5
|
+
"selectSubmenu": "Please select a submenu on the left"
|
|
6
|
+
},
|
|
7
|
+
"subscription": {
|
|
8
|
+
"plan": "Plan",
|
|
9
|
+
"status": "Status",
|
|
10
|
+
"startDate": "Start Date",
|
|
11
|
+
"endDate": "End Date",
|
|
12
|
+
"amount": "Amount",
|
|
13
|
+
"currency": "Currency",
|
|
14
|
+
"interval": "Interval",
|
|
15
|
+
"totalUsers": "Total Users",
|
|
16
|
+
"overdue": "Overdue",
|
|
17
|
+
"active": "Active",
|
|
18
|
+
"cancelled": "Cancelled",
|
|
19
|
+
"trialing": "Trialing",
|
|
20
|
+
"workspace": "Workspace",
|
|
21
|
+
"seats": "Seats",
|
|
22
|
+
"expires": "Expires",
|
|
23
|
+
"detail": "Details",
|
|
24
|
+
"action": "Actions",
|
|
25
|
+
"empty": "No subscriptions yet",
|
|
26
|
+
"notFound": "Subscription not found",
|
|
27
|
+
"count": "{{count}} subscriptions",
|
|
28
|
+
"detailTitle": "Subscription Detail",
|
|
29
|
+
"periodStart": "Period Start",
|
|
30
|
+
"periodEnd": "Period End",
|
|
31
|
+
"providerSubscription": "Provider Subscription",
|
|
32
|
+
"back": "Back to List",
|
|
33
|
+
"invoicesTitle": "Invoices",
|
|
34
|
+
"invoiceDate": "Date",
|
|
35
|
+
"invoiceAmount": "Amount",
|
|
36
|
+
"invoiceStatus": "Status"
|
|
37
|
+
},
|
|
38
|
+
"invoice": {
|
|
39
|
+
"number": "Invoice Number",
|
|
40
|
+
"date": "Invoice Date",
|
|
41
|
+
"dueDate": "Due Date",
|
|
42
|
+
"total": "Total",
|
|
43
|
+
"paid": "Paid",
|
|
44
|
+
"unpaid": "Unpaid",
|
|
45
|
+
"view": "View Invoice",
|
|
46
|
+
"usd": "$",
|
|
47
|
+
"cny": "¥"
|
|
48
|
+
},
|
|
49
|
+
"action": {
|
|
50
|
+
"upgrade": "Upgrade Plan",
|
|
51
|
+
"downgrade": "Downgrade Plan",
|
|
52
|
+
"cancel": "Cancel Subscription",
|
|
53
|
+
"retryPayment": "Retry Payment",
|
|
54
|
+
"exportData": "Export Data"
|
|
55
|
+
}
|
|
56
|
+
}
|