dsh-plugin-image-workbench 0.0.0-stage → 1.1.5
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/LICENSE +21 -0
- package/README.en.md +256 -0
- package/README.md +256 -2
- package/client.js +241 -0
- package/cordis.patch.yml +14 -0
- package/host.js +641 -0
- package/install.mjs +387 -0
- package/package.json +57 -4
- package/scripts/vendor-snapshot.py +154 -0
- package/server/access.py +26 -0
- package/server/app.py +1329 -0
- package/server/catalog.json +1 -0
- package/server/catalog.py +147 -0
- package/server/composer.py +391 -0
- package/server/drafts.py +158 -0
- package/server/history.py +575 -0
- package/server/imaging.py +172 -0
- package/server/pool.py +1335 -0
- package/server/pool_admin.py +921 -0
- package/server/pool_service.py +1073 -0
- package/server/requirements.txt +6 -0
- package/server/smoke_check.py +402 -0
- package/server/static/access.html +202 -0
- package/server/static/index.html +4725 -0
- package/vendor/chatgpt2api/README.md +13 -0
- package/vendor/chatgpt2api/SOURCE.json +11 -0
- package/vendor/chatgpt2api/runtime/uv.exe +0 -0
- package/vendor/chatgpt2api/source/.env.example +82 -0
- package/vendor/chatgpt2api/source/.python-version +1 -0
- package/vendor/chatgpt2api/source/LICENSE +21 -0
- package/vendor/chatgpt2api/source/README.md +391 -0
- package/vendor/chatgpt2api/source/VERSION +1 -0
- package/vendor/chatgpt2api/source/api/__init__.py +2 -0
- package/vendor/chatgpt2api/source/api/accounts.py +537 -0
- package/vendor/chatgpt2api/source/api/ai.py +217 -0
- package/vendor/chatgpt2api/source/api/app.py +62 -0
- package/vendor/chatgpt2api/source/api/errors.py +46 -0
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
- package/vendor/chatgpt2api/source/api/support.py +134 -0
- package/vendor/chatgpt2api/source/api/system.py +365 -0
- package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
- package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
- package/vendor/chatgpt2api/source/config.default.json +108 -0
- package/vendor/chatgpt2api/source/main.py +9 -0
- package/vendor/chatgpt2api/source/pyproject.toml +27 -0
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
- package/vendor/chatgpt2api/source/services/__init__.py +0 -0
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
- package/vendor/chatgpt2api/source/services/config.py +667 -0
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
- package/vendor/chatgpt2api/source/services/image_service.py +376 -0
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
- package/vendor/chatgpt2api/source/services/log_service.py +337 -0
- package/vendor/chatgpt2api/source/services/model_service.py +182 -0
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
- package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
- package/vendor/chatgpt2api/source/utils/helper.py +469 -0
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
- package/vendor/chatgpt2api/source/utils/log.py +110 -0
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
- package/vendor/chatgpt2api/source/utils/pow.py +207 -0
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
- package/vendor/chatgpt2api/source/uv.lock +904 -0
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
- package/vendor/chatgpt2api/source/web/components.json +24 -0
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
- package/vendor/chatgpt2api/source/web/package.json +58 -0
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
- package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
- package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ExternalLink, LoaderCircle, Save } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
7
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
|
|
10
|
+
import { useSettingsStore } from "../store";
|
|
11
|
+
|
|
12
|
+
export function ThirdPartyAppsCard() {
|
|
13
|
+
const config = useSettingsStore((state) => state.config);
|
|
14
|
+
const isLoadingConfig = useSettingsStore((state) => state.isLoadingConfig);
|
|
15
|
+
const isSavingConfig = useSettingsStore((state) => state.isSavingConfig);
|
|
16
|
+
const setInfiniteCanvasField = useSettingsStore((state) => state.setInfiniteCanvasField);
|
|
17
|
+
const saveConfig = useSettingsStore((state) => state.saveConfig);
|
|
18
|
+
|
|
19
|
+
if (isLoadingConfig || !config?.third_party_apps) {
|
|
20
|
+
return (
|
|
21
|
+
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
22
|
+
<CardContent className="flex items-center justify-center p-10">
|
|
23
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
24
|
+
</CardContent>
|
|
25
|
+
</Card>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const canvas = config.third_party_apps.infinite_canvas;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
33
|
+
<CardContent className="space-y-5 p-6">
|
|
34
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
35
|
+
<div>
|
|
36
|
+
<div className="flex items-center gap-2 text-base font-semibold text-stone-900">
|
|
37
|
+
<ExternalLink className="size-5 text-stone-500" />
|
|
38
|
+
无限画布入口
|
|
39
|
+
</div>
|
|
40
|
+
<p className="mt-1 text-xs leading-6 text-stone-500">开启后会在顶部导航显示入口,跳转时自动附带本项目地址和当前密钥。</p>
|
|
41
|
+
</div>
|
|
42
|
+
<span className={`rounded-full px-3 py-1 text-xs ${canvas.enabled ? "bg-emerald-50 text-emerald-700" : "bg-stone-100 text-stone-500"}`}>
|
|
43
|
+
{canvas.enabled ? "已启用" : "未启用"}
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="space-y-4 rounded-xl border border-stone-200 bg-white px-4 py-3">
|
|
48
|
+
<label className="flex items-center gap-3 text-sm text-stone-700">
|
|
49
|
+
<Checkbox
|
|
50
|
+
checked={Boolean(canvas.enabled)}
|
|
51
|
+
onCheckedChange={(checked) => setInfiniteCanvasField("enabled", Boolean(checked))}
|
|
52
|
+
/>
|
|
53
|
+
启用无限画布
|
|
54
|
+
</label>
|
|
55
|
+
<div className="space-y-2">
|
|
56
|
+
<label className="text-sm text-stone-700">无限画布地址</label>
|
|
57
|
+
<Input
|
|
58
|
+
value={canvas.url}
|
|
59
|
+
onChange={(event) => setInfiniteCanvasField("url", event.target.value)}
|
|
60
|
+
placeholder="https://canvas.best"
|
|
61
|
+
className="h-10 rounded-xl border-stone-200 bg-white"
|
|
62
|
+
/>
|
|
63
|
+
<p className="text-xs leading-5 text-stone-500">
|
|
64
|
+
顶部入口跳转时会追加 apiKey 和 baseUrl 参数;关闭后顶部导航不显示无限画布。
|
|
65
|
+
</p>
|
|
66
|
+
<p className="text-xs leading-5 text-amber-700">
|
|
67
|
+
该入口仅供个人测试使用;长期使用建议自行本机部署无限画布。
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className="flex justify-end">
|
|
73
|
+
<Button className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800" onClick={() => void saveConfig()} disabled={isSavingConfig}>
|
|
74
|
+
{isSavingConfig ? <LoaderCircle className="size-4 animate-spin" /> : <Save className="size-4" />}
|
|
75
|
+
保存
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</CardContent>
|
|
79
|
+
</Card>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { Ban, CheckCircle2, Copy, KeyRound, LoaderCircle, Pencil, Plus, Trash2 } from "lucide-react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
} from "@/components/ui/dialog";
|
|
18
|
+
import { Input } from "@/components/ui/input";
|
|
19
|
+
import { createUserKey, deleteUserKey, fetchUserKeys, updateUserKey, type UserKey } from "@/lib/api";
|
|
20
|
+
|
|
21
|
+
function formatDateTime(value?: string | null) {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return "—";
|
|
24
|
+
}
|
|
25
|
+
const date = new Date(value);
|
|
26
|
+
if (Number.isNaN(date.getTime())) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
30
|
+
year: "numeric",
|
|
31
|
+
month: "2-digit",
|
|
32
|
+
day: "2-digit",
|
|
33
|
+
hour: "2-digit",
|
|
34
|
+
minute: "2-digit",
|
|
35
|
+
}).format(date);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function UserKeysCard() {
|
|
39
|
+
const didLoadRef = useRef(false);
|
|
40
|
+
const [items, setItems] = useState<UserKey[]>([]);
|
|
41
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
42
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
43
|
+
const [name, setName] = useState("");
|
|
44
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
45
|
+
const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set());
|
|
46
|
+
const [revealedKey, setRevealedKey] = useState("");
|
|
47
|
+
const [deletingItem, setDeletingItem] = useState<UserKey | null>(null);
|
|
48
|
+
const [editingItem, setEditingItem] = useState<UserKey | null>(null);
|
|
49
|
+
const [editName, setEditName] = useState("");
|
|
50
|
+
const [editKey, setEditKey] = useState("");
|
|
51
|
+
|
|
52
|
+
const load = async () => {
|
|
53
|
+
setIsLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
const data = await fetchUserKeys();
|
|
56
|
+
setItems(data.items);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
toast.error(error instanceof Error ? error.message : "加载用户密钥失败");
|
|
59
|
+
} finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (didLoadRef.current) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
didLoadRef.current = true;
|
|
69
|
+
void load();
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const handleCreate = async () => {
|
|
73
|
+
setIsCreating(true);
|
|
74
|
+
try {
|
|
75
|
+
const data = await createUserKey(name.trim());
|
|
76
|
+
setItems(data.items);
|
|
77
|
+
setRevealedKey(data.key);
|
|
78
|
+
setName("");
|
|
79
|
+
setIsDialogOpen(false);
|
|
80
|
+
toast.success("用户密钥已创建");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
toast.error(error instanceof Error ? error.message : "创建用户密钥失败");
|
|
83
|
+
} finally {
|
|
84
|
+
setIsCreating(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const setItemPending = (id: string, isPending: boolean) => {
|
|
89
|
+
setPendingIds((current) => {
|
|
90
|
+
const next = new Set(current);
|
|
91
|
+
if (isPending) {
|
|
92
|
+
next.add(id);
|
|
93
|
+
} else {
|
|
94
|
+
next.delete(id);
|
|
95
|
+
}
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleToggle = async (item: UserKey) => {
|
|
101
|
+
setItemPending(item.id, true);
|
|
102
|
+
try {
|
|
103
|
+
const data = await updateUserKey(item.id, { enabled: !item.enabled });
|
|
104
|
+
setItems(data.items);
|
|
105
|
+
toast.success(item.enabled ? "用户密钥已禁用" : "用户密钥已启用");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
|
|
108
|
+
} finally {
|
|
109
|
+
setItemPending(item.id, false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleDelete = async () => {
|
|
114
|
+
if (!deletingItem) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const item = deletingItem;
|
|
118
|
+
setItemPending(item.id, true);
|
|
119
|
+
try {
|
|
120
|
+
const data = await deleteUserKey(item.id);
|
|
121
|
+
setItems(data.items);
|
|
122
|
+
setDeletingItem(null);
|
|
123
|
+
toast.success("用户密钥已删除");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
toast.error(error instanceof Error ? error.message : "删除用户密钥失败");
|
|
126
|
+
} finally {
|
|
127
|
+
setItemPending(item.id, false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const openEditDialog = (item: UserKey) => {
|
|
132
|
+
setEditingItem(item);
|
|
133
|
+
setEditName(item.name);
|
|
134
|
+
setEditKey("");
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleEdit = async () => {
|
|
138
|
+
if (!editingItem) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const item = editingItem;
|
|
142
|
+
const trimmedName = editName.trim();
|
|
143
|
+
const trimmedKey = editKey.trim();
|
|
144
|
+
if (trimmedName === item.name && !trimmedKey) {
|
|
145
|
+
setEditingItem(null);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setItemPending(item.id, true);
|
|
149
|
+
try {
|
|
150
|
+
const data = await updateUserKey(item.id, {
|
|
151
|
+
...(trimmedName !== item.name ? { name: trimmedName } : {}),
|
|
152
|
+
...(trimmedKey ? { key: trimmedKey } : {}),
|
|
153
|
+
});
|
|
154
|
+
setItems(data.items);
|
|
155
|
+
setEditingItem(null);
|
|
156
|
+
setEditKey("");
|
|
157
|
+
toast.success(trimmedKey ? "用户密钥已更新" : "用户名称已更新");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
|
|
160
|
+
} finally {
|
|
161
|
+
setItemPending(item.id, false);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleCopy = async (value: string) => {
|
|
166
|
+
try {
|
|
167
|
+
await navigator.clipboard.writeText(value);
|
|
168
|
+
toast.success("已复制到剪贴板");
|
|
169
|
+
} catch {
|
|
170
|
+
toast.error("复制失败,请手动复制");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
177
|
+
<CardContent className="space-y-6 p-6">
|
|
178
|
+
<div className="flex items-start justify-between gap-4">
|
|
179
|
+
<div className="flex items-center gap-3">
|
|
180
|
+
<div className="flex size-10 items-center justify-center rounded-xl bg-stone-100">
|
|
181
|
+
<KeyRound className="size-5 text-stone-600" />
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<h2 className="text-lg font-semibold tracking-tight">用户密钥管理</h2>
|
|
185
|
+
<p className="text-sm text-stone-500">为普通用户创建专用密钥;普通用户只能进入画图页,不能查看设置和号池。</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => setIsDialogOpen(true)}>
|
|
189
|
+
<Plus className="size-4" />
|
|
190
|
+
创建用户密钥
|
|
191
|
+
</Button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{revealedKey ? (
|
|
195
|
+
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-900">
|
|
196
|
+
<div className="font-medium">新密钥仅展示一次,请立即保存:</div>
|
|
197
|
+
<div className="mt-3 flex flex-col gap-3 rounded-lg border border-emerald-200 bg-white/80 p-3 md:flex-row md:items-center md:justify-between">
|
|
198
|
+
<code className="break-all font-mono text-[13px]">{revealedKey}</code>
|
|
199
|
+
<Button
|
|
200
|
+
type="button"
|
|
201
|
+
variant="outline"
|
|
202
|
+
className="h-9 rounded-xl border-emerald-200 bg-white px-4 text-emerald-700"
|
|
203
|
+
onClick={() => void handleCopy(revealedKey)}
|
|
204
|
+
>
|
|
205
|
+
<Copy className="size-4" />
|
|
206
|
+
复制
|
|
207
|
+
</Button>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
) : null}
|
|
211
|
+
|
|
212
|
+
{isLoading ? (
|
|
213
|
+
<div className="flex items-center justify-center py-10">
|
|
214
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
215
|
+
</div>
|
|
216
|
+
) : items.length === 0 ? (
|
|
217
|
+
<div className="rounded-xl bg-stone-50 px-6 py-10 text-center text-sm text-stone-500">
|
|
218
|
+
暂无普通用户密钥。点击右上角按钮后即可创建并分发给其他人。
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<div className="space-y-3">
|
|
222
|
+
{items.map((item) => {
|
|
223
|
+
const isPending = pendingIds.has(item.id);
|
|
224
|
+
return (
|
|
225
|
+
<div key={item.id} className="flex flex-col gap-3 rounded-xl border border-stone-200 bg-white px-4 py-4 md:flex-row md:items-center md:justify-between">
|
|
226
|
+
<div className="min-w-0 space-y-2">
|
|
227
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
228
|
+
<div className="truncate text-sm font-medium text-stone-800">{item.name}</div>
|
|
229
|
+
<Badge variant={item.enabled ? "success" : "secondary"} className="rounded-md">
|
|
230
|
+
{item.enabled ? "已启用" : "已禁用"}
|
|
231
|
+
</Badge>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500">
|
|
234
|
+
<span>创建时间 {formatDateTime(item.created_at)}</span>
|
|
235
|
+
<span>最近使用 {formatDateTime(item.last_used_at)}</span>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="flex items-center gap-2">
|
|
240
|
+
<Button
|
|
241
|
+
type="button"
|
|
242
|
+
variant="outline"
|
|
243
|
+
className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
|
|
244
|
+
onClick={() => openEditDialog(item)}
|
|
245
|
+
disabled={isPending}
|
|
246
|
+
>
|
|
247
|
+
{isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
|
|
248
|
+
编辑
|
|
249
|
+
</Button>
|
|
250
|
+
<Button
|
|
251
|
+
type="button"
|
|
252
|
+
variant="outline"
|
|
253
|
+
className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
|
|
254
|
+
onClick={() => void handleToggle(item)}
|
|
255
|
+
disabled={isPending}
|
|
256
|
+
>
|
|
257
|
+
{isPending ? (
|
|
258
|
+
<LoaderCircle className="size-4 animate-spin" />
|
|
259
|
+
) : item.enabled ? (
|
|
260
|
+
<Ban className="size-4" />
|
|
261
|
+
) : (
|
|
262
|
+
<CheckCircle2 className="size-4" />
|
|
263
|
+
)}
|
|
264
|
+
{item.enabled ? "禁用" : "启用"}
|
|
265
|
+
</Button>
|
|
266
|
+
<Button
|
|
267
|
+
type="button"
|
|
268
|
+
variant="outline"
|
|
269
|
+
className="h-9 rounded-xl border-rose-200 bg-white px-4 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
|
|
270
|
+
onClick={() => setDeletingItem(item)}
|
|
271
|
+
disabled={isPending}
|
|
272
|
+
>
|
|
273
|
+
{isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
|
274
|
+
删除
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</CardContent>
|
|
283
|
+
</Card>
|
|
284
|
+
|
|
285
|
+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
286
|
+
<DialogContent className="rounded-2xl p-6">
|
|
287
|
+
<DialogHeader className="gap-2">
|
|
288
|
+
<DialogTitle>创建用户密钥</DialogTitle>
|
|
289
|
+
<DialogDescription className="text-sm leading-6">
|
|
290
|
+
可选填写一个备注名称,方便区分不同使用者;创建后会生成一条只能查看一次的原始密钥。
|
|
291
|
+
</DialogDescription>
|
|
292
|
+
</DialogHeader>
|
|
293
|
+
<div className="space-y-2">
|
|
294
|
+
<label className="text-sm font-medium text-stone-700">名称(可选)</label>
|
|
295
|
+
<Input
|
|
296
|
+
value={name}
|
|
297
|
+
onChange={(event) => setName(event.target.value)}
|
|
298
|
+
placeholder="例如:设计同学 A、运营临时账号"
|
|
299
|
+
className="h-11 rounded-xl border-stone-200 bg-white"
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
<DialogFooter>
|
|
303
|
+
<Button
|
|
304
|
+
type="button"
|
|
305
|
+
variant="secondary"
|
|
306
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
307
|
+
onClick={() => setIsDialogOpen(false)}
|
|
308
|
+
disabled={isCreating}
|
|
309
|
+
>
|
|
310
|
+
取消
|
|
311
|
+
</Button>
|
|
312
|
+
<Button
|
|
313
|
+
type="button"
|
|
314
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
315
|
+
onClick={() => void handleCreate()}
|
|
316
|
+
disabled={isCreating}
|
|
317
|
+
>
|
|
318
|
+
{isCreating ? <LoaderCircle className="size-4 animate-spin" /> : <Plus className="size-4" />}
|
|
319
|
+
创建
|
|
320
|
+
</Button>
|
|
321
|
+
</DialogFooter>
|
|
322
|
+
</DialogContent>
|
|
323
|
+
</Dialog>
|
|
324
|
+
|
|
325
|
+
<Dialog open={Boolean(deletingItem)} onOpenChange={(open) => (!open ? setDeletingItem(null) : null)}>
|
|
326
|
+
<DialogContent className="rounded-2xl p-6">
|
|
327
|
+
<DialogHeader className="gap-2">
|
|
328
|
+
<DialogTitle>删除用户密钥</DialogTitle>
|
|
329
|
+
<DialogDescription className="text-sm leading-6">
|
|
330
|
+
确认删除用户密钥「{deletingItem?.name}」吗?删除后该密钥将无法继续调用接口。
|
|
331
|
+
</DialogDescription>
|
|
332
|
+
</DialogHeader>
|
|
333
|
+
<DialogFooter>
|
|
334
|
+
<Button
|
|
335
|
+
type="button"
|
|
336
|
+
variant="secondary"
|
|
337
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
338
|
+
onClick={() => setDeletingItem(null)}
|
|
339
|
+
disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
|
|
340
|
+
>
|
|
341
|
+
取消
|
|
342
|
+
</Button>
|
|
343
|
+
<Button
|
|
344
|
+
type="button"
|
|
345
|
+
className="h-10 rounded-xl bg-rose-600 px-5 text-white hover:bg-rose-700"
|
|
346
|
+
onClick={() => void handleDelete()}
|
|
347
|
+
disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
|
|
348
|
+
>
|
|
349
|
+
{deletingItem && pendingIds.has(deletingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
|
350
|
+
删除
|
|
351
|
+
</Button>
|
|
352
|
+
</DialogFooter>
|
|
353
|
+
</DialogContent>
|
|
354
|
+
</Dialog>
|
|
355
|
+
|
|
356
|
+
<Dialog
|
|
357
|
+
open={Boolean(editingItem)}
|
|
358
|
+
onOpenChange={(open) => {
|
|
359
|
+
if (!open) {
|
|
360
|
+
setEditingItem(null);
|
|
361
|
+
setEditKey("");
|
|
362
|
+
}
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
<DialogContent className="rounded-2xl p-6">
|
|
366
|
+
<DialogHeader className="gap-2">
|
|
367
|
+
<DialogTitle>编辑用户密钥</DialogTitle>
|
|
368
|
+
<DialogDescription className="text-sm leading-6">
|
|
369
|
+
可以修改备注名称;如需更换专用密钥,直接填写新的原始密钥即可。留空则保持当前密钥不变。
|
|
370
|
+
</DialogDescription>
|
|
371
|
+
</DialogHeader>
|
|
372
|
+
<div className="space-y-4">
|
|
373
|
+
<div className="space-y-2">
|
|
374
|
+
<label className="text-sm font-medium text-stone-700">名称</label>
|
|
375
|
+
<Input
|
|
376
|
+
value={editName}
|
|
377
|
+
onChange={(event) => setEditName(event.target.value)}
|
|
378
|
+
placeholder="例如:设计同学 A、运营临时账号"
|
|
379
|
+
className="h-11 rounded-xl border-stone-200 bg-white"
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
<div className="space-y-2">
|
|
383
|
+
<label className="text-sm font-medium text-stone-700">新的专用密钥(可选)</label>
|
|
384
|
+
<Input
|
|
385
|
+
value={editKey}
|
|
386
|
+
onChange={(event) => setEditKey(event.target.value)}
|
|
387
|
+
placeholder="例如:sk-your-custom-user-key"
|
|
388
|
+
className="h-11 rounded-xl border-stone-200 bg-white font-mono"
|
|
389
|
+
/>
|
|
390
|
+
<p className="text-xs leading-5 text-stone-500">
|
|
391
|
+
保存后旧密钥会立即失效,新密钥生效。系统仍只保存哈希,不会回显当前密钥。
|
|
392
|
+
</p>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<DialogFooter>
|
|
396
|
+
<Button
|
|
397
|
+
type="button"
|
|
398
|
+
variant="secondary"
|
|
399
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
400
|
+
onClick={() => {
|
|
401
|
+
setEditingItem(null);
|
|
402
|
+
setEditKey("");
|
|
403
|
+
}}
|
|
404
|
+
disabled={editingItem ? pendingIds.has(editingItem.id) : false}
|
|
405
|
+
>
|
|
406
|
+
取消
|
|
407
|
+
</Button>
|
|
408
|
+
<Button
|
|
409
|
+
type="button"
|
|
410
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
411
|
+
onClick={() => void handleEdit()}
|
|
412
|
+
disabled={editingItem ? pendingIds.has(editingItem.id) : false}
|
|
413
|
+
>
|
|
414
|
+
{editingItem && pendingIds.has(editingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
|
|
415
|
+
保存
|
|
416
|
+
</Button>
|
|
417
|
+
</DialogFooter>
|
|
418
|
+
</DialogContent>
|
|
419
|
+
</Dialog>
|
|
420
|
+
</>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { LoaderCircle } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
7
|
+
import { useAuthGuard } from "@/lib/use-auth-guard";
|
|
8
|
+
|
|
9
|
+
import { BackupSettingsCard } from "./components/backup-settings-card";
|
|
10
|
+
import { ApiDocsCard } from "./components/api-docs-card";
|
|
11
|
+
import { ConfigCard } from "./components/config-card";
|
|
12
|
+
import { CPAPoolDialog } from "./components/cpa-pool-dialog";
|
|
13
|
+
import { CPAPoolsCard } from "./components/cpa-pools-card";
|
|
14
|
+
import { ImportBrowserDialog } from "./components/import-browser-dialog";
|
|
15
|
+
import { ProxyRuntimeCard } from "./components/proxy-runtime-card";
|
|
16
|
+
import { SettingsHeader } from "./components/settings-header";
|
|
17
|
+
import { Sub2APIConnections } from "./components/sub2api-connections";
|
|
18
|
+
import { ThirdPartyAppsCard } from "./components/third-party-apps-card";
|
|
19
|
+
import { UserKeysCard } from "./components/user-keys-card";
|
|
20
|
+
import { useSettingsStore } from "./store";
|
|
21
|
+
|
|
22
|
+
const settingsTabs = [
|
|
23
|
+
{ value: "basic", title: "基础配置" },
|
|
24
|
+
{ value: "backup", title: "备份" },
|
|
25
|
+
{ value: "keys", title: "用户密钥" },
|
|
26
|
+
{ value: "api-docs", title: "接口接入" },
|
|
27
|
+
{ value: "canvas", title: "画布入口" },
|
|
28
|
+
{ value: "proxy", title: "FlareSolverr" },
|
|
29
|
+
{ value: "cpa", title: "CPA" },
|
|
30
|
+
{ value: "sub2api", title: "Sub2API" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function SettingsDataController() {
|
|
34
|
+
const didLoadRef = useRef(false);
|
|
35
|
+
const initialize = useSettingsStore((state) => state.initialize);
|
|
36
|
+
const loadPools = useSettingsStore((state) => state.loadPools);
|
|
37
|
+
const loadBackups = useSettingsStore((state) => state.loadBackups);
|
|
38
|
+
const pools = useSettingsStore((state) => state.pools);
|
|
39
|
+
const backupState = useSettingsStore((state) => state.backupState);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (didLoadRef.current) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
didLoadRef.current = true;
|
|
46
|
+
void initialize();
|
|
47
|
+
}, [initialize]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const hasRunningJobs = pools.some((pool) => {
|
|
51
|
+
const status = pool.import_job?.status;
|
|
52
|
+
return status === "pending" || status === "running";
|
|
53
|
+
});
|
|
54
|
+
if (!hasRunningJobs) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timer = window.setInterval(() => {
|
|
59
|
+
void loadPools(true);
|
|
60
|
+
}, 1500);
|
|
61
|
+
return () => window.clearInterval(timer);
|
|
62
|
+
}, [loadPools, pools]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!backupState?.running) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const timer = window.setInterval(() => {
|
|
69
|
+
void loadBackups(true);
|
|
70
|
+
}, 3000);
|
|
71
|
+
return () => window.clearInterval(timer);
|
|
72
|
+
}, [backupState?.running, loadBackups]);
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function SettingsPageContent() {
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<SettingsDataController />
|
|
81
|
+
<SettingsHeader />
|
|
82
|
+
<Tabs defaultValue="basic" className="space-y-4">
|
|
83
|
+
<div className="sticky top-3 z-20 overflow-x-auto rounded-xl border border-white/80 bg-white/90 px-3 py-2 shadow-sm backdrop-blur">
|
|
84
|
+
<TabsList variant="line" className="min-w-max justify-start">
|
|
85
|
+
{settingsTabs.map((tab) => (
|
|
86
|
+
<TabsTrigger key={tab.value} value={tab.value} className="px-4">
|
|
87
|
+
{tab.title}
|
|
88
|
+
</TabsTrigger>
|
|
89
|
+
))}
|
|
90
|
+
</TabsList>
|
|
91
|
+
</div>
|
|
92
|
+
<TabsContent value="basic">
|
|
93
|
+
<ConfigCard />
|
|
94
|
+
</TabsContent>
|
|
95
|
+
<TabsContent value="proxy">
|
|
96
|
+
<ProxyRuntimeCard />
|
|
97
|
+
</TabsContent>
|
|
98
|
+
<TabsContent value="backup">
|
|
99
|
+
<BackupSettingsCard />
|
|
100
|
+
</TabsContent>
|
|
101
|
+
<TabsContent value="keys">
|
|
102
|
+
<UserKeysCard />
|
|
103
|
+
</TabsContent>
|
|
104
|
+
<TabsContent value="canvas">
|
|
105
|
+
<ThirdPartyAppsCard />
|
|
106
|
+
</TabsContent>
|
|
107
|
+
<TabsContent value="api-docs">
|
|
108
|
+
<ApiDocsCard />
|
|
109
|
+
</TabsContent>
|
|
110
|
+
<TabsContent value="cpa">
|
|
111
|
+
<CPAPoolsCard />
|
|
112
|
+
</TabsContent>
|
|
113
|
+
<TabsContent value="sub2api">
|
|
114
|
+
<Sub2APIConnections />
|
|
115
|
+
</TabsContent>
|
|
116
|
+
</Tabs>
|
|
117
|
+
<CPAPoolDialog />
|
|
118
|
+
<ImportBrowserDialog />
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default function SettingsPage() {
|
|
124
|
+
const { isCheckingAuth, session } = useAuthGuard(["admin"]);
|
|
125
|
+
|
|
126
|
+
if (isCheckingAuth || !session || session.role !== "admin") {
|
|
127
|
+
return (
|
|
128
|
+
<div className="flex min-h-[40vh] items-center justify-center">
|
|
129
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return <SettingsPageContent />;
|
|
135
|
+
}
|