Merge branch 'ji' into master
按钮管理「登录参数」两套实现冲突解决——两分支各自独立给跳转按钮加了登录态追加能力, 二者正交、保留两套,互不影响: - master: login_params (JSON 列 login_params_json,从 userInfo 解析,类似轻应用登录参数) - ji: append_auth + auth_token_param + auth_name_param (token 登录时追加 token1+用户名) 迁移 revision 碰撞修复(两分支都用了 20260625_01):重编号为线性链 20260620_02 → 20260625_01 (login_params) → 20260625_02 (auth_params) → 20260625_03 (users_approved) 并同步 column_additions_mysql.sql 注释里的 migration 编号。 前后端 task_buttons(model/sql/memory/router/前端 type·api·admin 页)均保留两套字段; 两份 CLAUDE.md 文档合并为同时记录两种机制。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
58357547c9
44 files changed
+1207
-46
No files matched your search
@@ -11,6 +11,8 @@ export interface UserListItem {
|
||||
agent_count: number;
|
||||
/** 问答次数:用户提问的次数(一次提问算一次)。 */
|
||||
qa_count: number;
|
||||
/** 登录白名单放行标记:开关开启后仅 approved 用户(或 admin)可登录。 */
|
||||
approved: boolean;
|
||||
}
|
||||
|
||||
export async function listAllUsers(): Promise<UserListItem[]> {
|
||||
@@ -19,6 +21,20 @@ export async function listAllUsers(): Promise<UserListItem[]> {
|
||||
return res.json() as Promise<UserListItem[]>;
|
||||
}
|
||||
|
||||
/** 放行 / 取消放行某个注册用户(登录白名单)。Admin only。 */
|
||||
export async function setUserApproval(userId: string, approved: boolean): Promise<UserListItem> {
|
||||
const res = await apiFetch(`${getBackendBaseURL()}/api/v1/auth/users/${encodeURIComponent(userId)}/approval`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ approved }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
throw new Error(extractErrorMessage(data, "保存失败"));
|
||||
}
|
||||
return res.json() as Promise<UserListItem>;
|
||||
}
|
||||
|
||||
// Normalize a FastAPI error body into a human-readable string. `detail` may be
|
||||
// a plain string, an object like AuthErrorResponse ({ code, message }), or an
|
||||
// array of validation errors — anything but a string would otherwise render as
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { listAllUsers } from "./api";
|
||||
import { OptType, writeYLog } from "@/core/log/y-log";
|
||||
|
||||
import { listAllUsers, setUserApproval } from "./api";
|
||||
|
||||
export function useAllUsers(enabled: boolean) {
|
||||
return useQuery({
|
||||
@@ -9,3 +11,20 @@ export function useAllUsers(enabled: boolean) {
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/** 放行 / 取消放行某个用户(登录白名单),成功后刷新用户列表缓存。 */
|
||||
export function useSetUserApproval() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, approved }: { userId: string; approved: boolean }) => setUserApproval(userId, approved),
|
||||
onSuccess: (_data, vars) => {
|
||||
writeYLog({
|
||||
optType: vars.approved ? OptType.enabled : OptType.disabled,
|
||||
optDetail: `CM助手-全量-白名单管理-${vars.approved ? "放行" : "取消放行"}用户`,
|
||||
url: `/api/v1/auth/users/${vars.userId}/approval`,
|
||||
reqParam: vars,
|
||||
});
|
||||
void queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
resolveTaskDeeplinkTarget,
|
||||
} from "@/core/auth/task-deeplink";
|
||||
import { readUserInfo, resolveRouteParamValue } from "@/strategy-components/lib/light-app";
|
||||
import {
|
||||
readLoginToken,
|
||||
readLoginUsername,
|
||||
} from "@/core/auth/login-credentials";
|
||||
|
||||
/** 「大屏绘制」独立页应用内路由(深链「上一步」默认指向它,跳转追加 ?taskId=)。 */
|
||||
export const DASHBOARD_AGENT_ROUTE = "/page/strategy/dashboard-agent";
|
||||
@@ -102,6 +106,16 @@ export interface TaskButton {
|
||||
openMode: TaskButtonOpenMode;
|
||||
/** ``url`` 外链追加的登录参数列表(仅 ``url`` 类型有意义;为空则不追加,行为同旧版)。 */
|
||||
loginParams: TaskButtonLoginParam[];
|
||||
/**
|
||||
* ``url`` 跳转是否追加登录态:token1(token 登录的上游 token)+ 登录用户名,供外链所在的
|
||||
* 外部系统识别当前登录用户。**仅 token 登录**(前端存有 token1,见 [[login-credentials]])时
|
||||
* 实际拼上;非 token 登录读不到 token1,自动跳过(按钮照常跳,只是不带这两个参数)。
|
||||
*/
|
||||
appendAuth: boolean;
|
||||
/** 追加 token 的查询参数名(空则用 ``token``)。 */
|
||||
authTokenParam: string;
|
||||
/** 追加用户名的查询参数名(空则用 ``username``)。 */
|
||||
authNameParam: string;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
@@ -154,6 +168,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
});
|
||||
@@ -169,6 +186,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
});
|
||||
@@ -185,6 +205,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 0,
|
||||
},
|
||||
@@ -204,6 +227,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idKind: "task",
|
||||
openMode: "self",
|
||||
loginParams: [],
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 1,
|
||||
},
|
||||
@@ -218,6 +244,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 2,
|
||||
},
|
||||
@@ -232,6 +261,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: 3,
|
||||
},
|
||||
@@ -309,6 +341,18 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro
|
||||
url = appendQueryParam(url, key, resolveLoginParamValue(lp, userInfo));
|
||||
}
|
||||
}
|
||||
// 追加登录态:仅 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("//")) {
|
||||
|
||||
Reference in new issue
Block a user