feat(task-buttons): 跳转按钮可追加登录态 token+username + token 登录离线测试映射
rwfx/3qfx/xdfx 任务工作区的可配置跳转按钮(按钮管理)新增「追加登录态」开关: url 类型按钮可在外链后拼 ?token=<token1>&username=<登录名>(参数名可配,默认 token/username),供跳转目标外部系统识别当前登录用户。 - 仅 token 登录(login/token)生效: token1 = ?authToken=,username = 后端用 token 调 getTokenInfo 解析出的原始登录名(非邮箱前缀/非解 JWT)。二者登录时 经 persistLoginCredentials 存入 login.token1/login.username(scoped-storage 自动隔离嵌入/独立会话);非 token 登录读不到 token1 则跳过追加。 - 后端 UsernameLoginResponse 回传 username(原始名,规避邮箱归一化有损); token_login 加 mock_tokens 离线测试映射(123ewq->admin),命中即跳过外网 getTokenInfo,便于本地全链路联调。 - task_buttons 加列 append_auth/auth_token_param/auth_name_param (migration 20260625_01 + 幂等 SQL)。 Tests: test_task_buttons.py, test_auth_login_modes.py(mock token/username 回传)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6989dd45f1
commit
f8b40353ef
18 files changed
+298
-6
No files matched your search
@@ -7,6 +7,12 @@ export interface LoginByUsernameResponse {
|
||||
system_role: string;
|
||||
needs_setup: boolean;
|
||||
created: boolean;
|
||||
/**
|
||||
* 本次登录解析出的原始登录用户名(token 登录=getTokenInfo 解析值;用户名登录=所传用户名)。
|
||||
* 用于跳转外链时拼 ``?username=``——后端落库的邮箱前缀是归一化(小写/替换)后的,有损,故以此为准。
|
||||
* 旧后端可能不返回。
|
||||
*/
|
||||
username?: string;
|
||||
}
|
||||
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 任务工作区跳转外链时要拼接的「登录态凭据」:token1 + 登录用户名 的存取。
|
||||
*
|
||||
* - **token1** = token 登录用的上游 token(登录 URL 的 ``?authToken=``,即走
|
||||
* ``/api/v1/auth/login/token`` 这条 token 换登录路径用的 token)。
|
||||
* - **username** = 后端 token 登录时用该 token 调 ``getTokenInfo`` 解析出的**登录用户名**
|
||||
* (见 ``auth.py`` ``/login/token`` → ``UsernameLoginResponse.username``)。**不是**邮箱
|
||||
* 前缀、也**不是**解 JWT 得来——后端把它落库时会 ``.lower()`` + 归一化,邮箱前缀是有损的,
|
||||
* 所以以后端回传的原始值为准。
|
||||
*
|
||||
* 二者均经 [[scoped-storage]] 的 ``authStorage*`` 存取(键用 ``login.`` 前缀,属其受保护
|
||||
* 鉴权键族):嵌入(iframe)/独立会话态自动落 sessionStorage 隔离、正常态落 localStorage,
|
||||
* 并随「开启独立会话」一起被 ``migrateAuthToSession`` 迁移。
|
||||
*
|
||||
* **门控**:仅 token 登录(``authToken`` 存在)才写入。非 token 登录不写 token1 → 跳转按钮
|
||||
* 的「追加登录态」开关此时读不到 token,自然不追加(符合「必须用 login/token 登录才能带
|
||||
* token 跳转」)。
|
||||
*/
|
||||
|
||||
import {
|
||||
authStorageGet,
|
||||
authStorageRemove,
|
||||
authStorageSet,
|
||||
} from "@/core/auth/scoped-storage";
|
||||
import { getAccountDisplayName, type LoginByUsernameResponse } from "@/core/auth";
|
||||
|
||||
const LOGIN_TOKEN_KEY = "login.token1";
|
||||
const LOGIN_USERNAME_KEY = "login.username";
|
||||
|
||||
/**
|
||||
* 持久化 token 登录的 token1 + 用户名(token 登录成功后调用)。
|
||||
* ``authToken`` 为空则整次忽略(非 token 登录不留痕)。
|
||||
*/
|
||||
export function persistLoginCredentials(
|
||||
authToken: string | null | undefined,
|
||||
auth: LoginByUsernameResponse | null,
|
||||
): void {
|
||||
const token = authToken?.trim();
|
||||
if (!token) return;
|
||||
authStorageSet(LOGIN_TOKEN_KEY, token);
|
||||
// 优先用后端回传的原始登录名(getTokenInfo 解析值);旧后端无该字段时退回账号显示名
|
||||
// (邮箱 @ 前缀)兜底,保证总有个值可用。
|
||||
const username = auth?.username?.trim() || getAccountDisplayName(auth);
|
||||
if (username) authStorageSet(LOGIN_USERNAME_KEY, username);
|
||||
}
|
||||
|
||||
/** 读 token1(无则 ``null`` → 跳转不追加登录态)。 */
|
||||
export function readLoginToken(): string | null {
|
||||
return authStorageGet(LOGIN_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/** 读登录用户名(无则 ``null``)。 */
|
||||
export function readLoginUsername(): string | null {
|
||||
return authStorageGet(LOGIN_USERNAME_KEY);
|
||||
}
|
||||
|
||||
/** 清除登录态凭据(登出时调用)。 */
|
||||
export function clearLoginCredentials(): void {
|
||||
authStorageRemove(LOGIN_TOKEN_KEY);
|
||||
authStorageRemove(LOGIN_USERNAME_KEY);
|
||||
}
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
readCopTaskDetail,
|
||||
resolveTaskDeeplinkTarget,
|
||||
} from "@/core/auth/task-deeplink";
|
||||
import {
|
||||
readLoginToken,
|
||||
readLoginUsername,
|
||||
} from "@/core/auth/login-credentials";
|
||||
|
||||
/** 「大屏绘制」独立页应用内路由(深链「上一步」默认指向它,跳转追加 ?taskId=)。 */
|
||||
export const DASHBOARD_AGENT_ROUTE = "/page/strategy/dashboard-agent";
|
||||
@@ -85,6 +89,16 @@ export interface TaskButton {
|
||||
idKind: TaskButtonIdKind;
|
||||
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。仅 ``url`` 类型有意义。 */
|
||||
openMode: TaskButtonOpenMode;
|
||||
/**
|
||||
* ``url`` 跳转是否追加登录态:token1(token 登录的上游 token)+ 登录用户名,供外链所在的
|
||||
* 外部系统识别当前登录用户。**仅 token 登录**(前端存有 token1,见 [[login-credentials]])时
|
||||
* 实际拼上;非 token 登录读不到 token1,自动跳过(按钮照常跳,只是不带这两个参数)。
|
||||
*/
|
||||
appendAuth: boolean;
|
||||
/** 追加 token 的查询参数名(空则用 ``token``)。 */
|
||||
authTokenParam: string;
|
||||
/** 追加用户名的查询参数名(空则用 ``username``)。 */
|
||||
authNameParam: string;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
@@ -120,6 +134,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
});
|
||||
@@ -134,6 +151,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
});
|
||||
@@ -149,6 +169,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 0,
|
||||
},
|
||||
@@ -167,6 +190,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "taskId",
|
||||
idKind: "task",
|
||||
openMode: "self",
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 1,
|
||||
},
|
||||
@@ -180,6 +206,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 2,
|
||||
},
|
||||
@@ -193,6 +222,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 3,
|
||||
},
|
||||
@@ -259,7 +291,19 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro
|
||||
// 行动分析(xdfx)可选追加行动id;否则用任务id。行动id 缺失时回退任务id。
|
||||
const id = btn.idKind === "action" ? ctx.actionId || ctx.taskId : ctx.taskId;
|
||||
const param = (btn.idParam || "id").trim() || "id";
|
||||
const url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base;
|
||||
let url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base;
|
||||
// 追加登录态:仅 token 登录(存在 token1)才拼 token + 用户名,供外链所在的外部系统识别
|
||||
// 当前登录用户。非 token 登录读不到 token1 → 整段跳过(不影响跳转本身)。
|
||||
if (btn.appendAuth) {
|
||||
const token1 = readLoginToken();
|
||||
if (token1) {
|
||||
url = appendQueryParam(url, (btn.authTokenParam || "token").trim() || "token", token1);
|
||||
const name = readLoginUsername();
|
||||
if (name) {
|
||||
url = appendQueryParam(url, (btn.authNameParam || "username").trim() || "username", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 应用内部路由(以单个 / 开头,如「上一步」→ 大屏绘制页):
|
||||
// self → 走 react-router 同窗导航(不刷新、保留 SPA);blank → 拼成当前源的 hash 路由新标签打开。
|
||||
if (base.startsWith("/") && !base.startsWith("//")) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from '@/core/page-layout/sidebar-enabled';
|
||||
import { fetchYLogConfig } from '@/core/log/y-log-config';
|
||||
import { persistYLogToken } from '@/core/log/y-log-token';
|
||||
import { persistLoginCredentials } from '@/core/auth/login-credentials';
|
||||
import { OptType, writeYLog } from '@/core/log/y-log';
|
||||
|
||||
const DEFAULT_USERNAME = 'guest';
|
||||
@@ -167,6 +168,9 @@ export default function LoginPage() {
|
||||
// 持久化 token1(走 authStorage,嵌入态自动隔离)+ 预热上报配置 + 上报登录。
|
||||
if (authToken) {
|
||||
persistYLogToken(authToken);
|
||||
// 存下 token1 + 登录用户名(getTokenInfo 解析出的原始名,后端回传),供任务工作区
|
||||
// 跳转按钮的「追加登录态」拼到外链上(仅 token 登录才有这两个值)。
|
||||
persistLoginCredentials(authToken, auth);
|
||||
void fetchYLogConfig();
|
||||
writeYLog({
|
||||
optType: OptType.login,
|
||||
|
||||
@@ -105,6 +105,9 @@ export default function TaskButtonsPage() {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: maxSort + 1,
|
||||
},
|
||||
@@ -319,6 +322,37 @@ export default function TaskButtonsPage() {
|
||||
<SelectItem value="self">当前窗口</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex shrink-0 items-center gap-1.5 text-xs"
|
||||
title="跳转时在链接后追加 token + 用户名(仅 token 登录时生效)"
|
||||
>
|
||||
<Switch
|
||||
checked={r.appendAuth}
|
||||
onCheckedChange={(v) => update(r.id, { appendAuth: v })}
|
||||
/>
|
||||
追加登录态
|
||||
</label>
|
||||
{r.appendAuth && (
|
||||
<div className="flex shrink-0 items-center gap-1 text-xs">
|
||||
<span className="text-muted-foreground">&</span>
|
||||
<Input
|
||||
value={r.authTokenParam}
|
||||
onChange={(e) => update(r.id, { authTokenParam: e.target.value })}
|
||||
placeholder="token"
|
||||
className="h-8 w-24"
|
||||
title="token 的查询参数名(默认 token)"
|
||||
/>
|
||||
<span className="text-muted-foreground">=token1 &</span>
|
||||
<Input
|
||||
value={r.authNameParam}
|
||||
onChange={(e) => update(r.id, { authNameParam: e.target.value })}
|
||||
placeholder="username"
|
||||
className="h-8 w-28"
|
||||
title="用户名的查询参数名(默认 username)"
|
||||
/>
|
||||
<span className="text-muted-foreground">=用户名</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Select value={r.target} onValueChange={(v) => update(r.id, { target: v })}>
|
||||
|
||||
@@ -31,6 +31,9 @@ interface ServerButton {
|
||||
id_param: string;
|
||||
id_kind: string;
|
||||
open_mode: string;
|
||||
append_auth: boolean;
|
||||
auth_token_param: string;
|
||||
auth_name_param: string;
|
||||
enabled: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
@@ -46,6 +49,9 @@ function toButton(s: ServerButton): TaskButton {
|
||||
idParam: s.id_param || "id",
|
||||
idKind: (s.id_kind as TaskButtonIdKind) ?? "task",
|
||||
openMode: (s.open_mode as TaskButtonOpenMode) ?? "blank",
|
||||
appendAuth: s.append_auth ?? false,
|
||||
authTokenParam: s.auth_token_param || "token",
|
||||
authNameParam: s.auth_name_param || "username",
|
||||
enabled: s.enabled ?? true,
|
||||
sortOrder: s.sort_order ?? 0,
|
||||
};
|
||||
@@ -62,6 +68,9 @@ function toBody(b: TaskButton): ServerButton {
|
||||
id_param: b.idParam,
|
||||
id_kind: b.idKind,
|
||||
open_mode: b.openMode,
|
||||
append_auth: b.appendAuth,
|
||||
auth_token_param: b.authTokenParam,
|
||||
auth_name_param: b.authNameParam,
|
||||
enabled: b.enabled,
|
||||
sort_order: b.sortOrder,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user