Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/components/agents/AddAgentDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useKv } from "@/composables/useKv";
import { useBackendExtra } from "@/composables/useBackendExtra";
import { useLifecycle } from "@/composables/useLifecycle";
import { reGenerateToken } from "@/components/agents/generateToken";
import { toast } from "vue-sonner";

const open = defineModel<boolean>("open", { required: true });
const emit = defineEmits<{
Expand Down Expand Up @@ -267,7 +268,12 @@ const canNext = computed(() => {
const handleNext = async () => {
if (step.value === 1) {
// 预生成 token
generatedToken.value = (await reGenerateToken(nodeUuid.value)) || "";
const newToken = await reGenerateToken(nodeUuid.value);
if (!newToken) {
toast.error("预生成 token 失败,可能会导致安装失败,请重试");
return;
}
generatedToken.value = newToken;
step.value = 2;
loadCrons();
} else if (step.value === 2) {
Expand Down
24 changes: 17 additions & 7 deletions src/components/agents/generateToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@ import { getWsConnection } from "@/composables/useWsConnection";
import { AGENT_TEMPLATE_PERMISSIONS } from "@/components/token/tokenTemplates.ts";
import { generatePassword } from "@/lib/password";
import { makeRpcFunction } from "@/composables/useWsConnection";
import {
decodeTokenUsername,
encodeTokenUsername,
} from "@/components/token/scopeCodec";

const { currentBackend } = useBackendStore();

// 主控自 2026-07-29 起拒绝包含 ':' 或 '|' 的 username(这两个字符用于区分
// "key:secret" / "username|password" 两种鉴权格式),因此发往后端时统一 URI 编码。
export const agentUsername = (nodeUuid: string) =>
encodeTokenUsername(`[agent]:${nodeUuid}`);

// 展示层用:把后端存储的 username 安全解码为可读原文。
export const displayTokenUsername = (
username: string | null | undefined,
): string => (username ? decodeTokenUsername(username) : "");

function makeTokenObject(nodeUuid: string) {
return {
username: `[agent]:${nodeUuid}`,
username: agentUsername(nodeUuid),
password: generatePassword(16),
timestamp_from: null,
timestamp_to: null,
Expand Down Expand Up @@ -61,7 +75,7 @@ export async function reGenerateToken(
secret?: string;
}>("token_delete", {
token: backend.value.token,
target_token: `[agent]:${nodeUuid}`,
target_token: agentUsername(nodeUuid),
});
} catch {}

Expand All @@ -78,13 +92,9 @@ export async function upgradeTokenLimit(
if (!backend.value) return;
const rpc = makeRpcFunction();
try {
// const tokenDetail = await rpc<Token>("token_edit", {
// "token":`[agent]:${nodeUuid}`,
// "supertoken":backend.value?.token || ''
// })
rpc("token_edit", {
token: backend.value.token,
target_token: `[agent]:${nodeUuid}`,
target_token: agentUsername(nodeUuid),
limit: makeTokenObject(nodeUuid).token_limit,
});
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/node/setting/NodeSettingTabDelete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
type splitConfig,
} from "@/composables/useAgentConfig";
import { compareVersions } from "compare-versions";
import { agentUsername } from "@/components/agents/generateToken";

const props = defineProps<{ uuid: string }>();

Expand Down Expand Up @@ -135,7 +136,7 @@ async function handleDelete() {
// disable token, stop data report
await rpc("token_delete", {
token: currentBackend.value?.token,
target_token: `[agent]:${props.uuid}`,
target_token: agentUsername(props.uuid),
});
} catch {}
setStep(0, "done");
Expand Down
9 changes: 5 additions & 4 deletions src/components/token/components/tokenDetailPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Spinner } from "@/components/ui/spinner";
import { displayTokenUsername } from "@/components/agents/generateToken";

type PermissionSection = {
title: string;
Expand Down Expand Up @@ -245,7 +246,7 @@ const rawSummary = computed(() => {
<div class="text-sm">
{{
displayText(
rawSummary.username ?? token.username,
displayTokenUsername(rawSummary.username ?? token.username),
t("dashboard.token.detail.preview.notSet"),
)
}}
Expand Down Expand Up @@ -293,7 +294,7 @@ const rawSummary = computed(() => {
<CollapsibleTrigger as-child>
<button
type="button"
class="flex w-full flex-col gap-3 rounded-md text-left transition-colors hover:bg-muted/40 focus-visible:ring-ring/50 focus-visible:outline-none focus-visible:ring-[1px] lg:flex-row lg:items-center lg:justify-between"
class="flex w-full flex-col gap-3 rounded-md text-left transition-colors hover:bg-muted/40 focus-visible:ring-[1px] focus-visible:ring-ring/50 focus-visible:outline-none lg:flex-row lg:items-center lg:justify-between"
>
<div class="flex items-center gap-2">
<CardTitle>{{
Expand Down Expand Up @@ -369,14 +370,14 @@ const rawSummary = computed(() => {
</div>
</div>

<Card class="h-fit min-w-0 max-w-full overflow-hidden">
<Card class="h-fit max-w-full min-w-0 overflow-hidden">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<ScanEye class="h-5 w-5" />
{{ t("dashboard.token.detail.preview.rawJsonTitle") }}
</CardTitle>
</CardHeader>
<CardContent class="min-w-0 max-w-full">
<CardContent class="max-w-full min-w-0">
<div class="w-full max-w-full overflow-x-auto rounded-lg bg-muted/40">
<pre
class="max-h-[720px] w-max min-w-full overflow-y-auto p-4 text-xs leading-6"
Expand Down
18 changes: 16 additions & 2 deletions src/components/token/scopeCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ import type {

export const DEFAULT_SCOPE: TokenLimitScope = [{ global: null }];

// 主控拒绝包含 ':' 或 '|' 的 username(这两个字符用于区分 "key:secret" /
// "username|password" 两种鉴权格式)。因此发往后端的 username 一律 URI 编码,
// 展示与用户输入层使用原文,读取时经 decodeTokenUsername 安全解码。
export const encodeTokenUsername = (username: string): string =>
encodeURIComponent(username);

export const decodeTokenUsername = (username: string): string => {
try {
return decodeURIComponent(username);
} catch {
return username;
}
};

export const createDefaultToken = (): Token => ({
version: 1,
timestamp_from: 0,
Expand Down Expand Up @@ -172,7 +186,7 @@ export const buildCredentialPayload = (
const password = source.password.trim();

return {
...(username ? { username } : {}),
...(username ? { username: encodeTokenUsername(username) } : {}),
...(password ? { password } : {}),
};
};
Expand Down Expand Up @@ -207,7 +221,7 @@ export const mapTokenDetailToForm = (detail: TokenDetail | null): Token => {

return {
version: detail.version ?? 1,
username: detail.username ?? "",
username: detail.username ? decodeTokenUsername(detail.username) : "",
password: detail.password ?? "",
timestamp_from: detail.timestamp_from ?? 0,
timestamp_to: detail.timestamp_to ?? 0,
Expand Down
17 changes: 13 additions & 4 deletions src/components/token/token-list/tokenListCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from "lucide-vue-next";
import { useTokenListHook, type Token } from "@/composables/token/useTokenList";
import { getPasswordChangeValidationError } from "@/composables/token/tokenSecret";
import { displayTokenUsername } from "@/components/agents/generateToken";
import TokenSuccessDialog from "../components/TokenSuccessDialog.vue";

const useTokenList = useTokenListHook();
Expand Down Expand Up @@ -82,10 +83,18 @@ const normalizeSearchText = (value: string | null | undefined) =>

const filteredTokens = computed(() => {
const keyword = normalizeSearchText(debouncedSearchKeyword.value);
const encodedKeyword = normalizeSearchText(encodeURIComponent(keyword));

return tokensList.value.filter((token) => {
const storedUsername = token.username ?? "";
const matchesUsername =
!keyword || normalizeSearchText(token.username).includes(keyword);
!keyword ||
normalizeSearchText(displayTokenUsername(storedUsername)).includes(
keyword,
) ||
normalizeSearchText(storedUsername)
.replace(/%([0-9a-f]{2})/gi, (m) => m.toUpperCase())
.includes(encodedKeyword.toUpperCase());
const matchesTokenKey =
!keyword || normalizeSearchText(token.token_key).includes(keyword);

Expand Down Expand Up @@ -351,7 +360,7 @@ watch(changePasswordOpen, (open) => {
<TableBody>
<TableRow v-for="token in pagedTokens" :key="token.token_key">
<TableCell>{{ token.version }}</TableCell>
<TableCell>{{ token.username }}</TableCell>
<TableCell>{{ displayTokenUsername(token.username) }}</TableCell>
<TableCell class="font-mono">{{ token.token_key }}</TableCell>
<TableCell>{{ token.token_limit?.length ?? 0 }}</TableCell>
<TableCell class="flex w-32 gap-2">
Expand Down Expand Up @@ -500,7 +509,7 @@ watch(changePasswordOpen, (open) => {
{{ t("dashboard.token.list.table.username") }}:
</span>
<span class="ml-2">
{{ selectedResetToken?.username || "-" }}
{{ displayTokenUsername(selectedResetToken?.username) || "-" }}
</span>
</div>
<div>
Expand Down Expand Up @@ -551,7 +560,7 @@ watch(changePasswordOpen, (open) => {
{{ t("dashboard.token.list.table.username") }}:
</span>
<span class="ml-2">
{{ selectedPasswordToken?.username || "-" }}
{{ displayTokenUsername(selectedPasswordToken?.username) || "-" }}
</span>
</div>
<div>
Expand Down
6 changes: 5 additions & 1 deletion src/composables/useThemeTokenPresets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getWsConnection } from "@/composables/useWsConnection";
import {
createDefaultToken,
DEFAULT_SCOPE,
encodeTokenUsername,
serializeTokenPayload,
} from "@/components/token/scopeCodec";
import { generatePassword } from "@/lib/password";
Expand Down Expand Up @@ -61,7 +62,10 @@ export function useThemeTokenPresets() {
const listResult = await conn.call<{
tokens?: Array<{ token_key: string; username: string | null }>;
}>("token_list_all_tokens", { token: fatherToken });
const existing = listResult.tokens?.find((t) => t.username === username);
const existing = listResult.tokens?.find(
(t) =>
t.username === encodeTokenUsername(username) || t.username === username,
);
if (existing) {
await conn.call("token_delete", {
token: fatherToken,
Expand Down
Loading