统一外部 access_token 存储与轻应用 URL 拼接,修复嵌入会话鉴权隔离。
将 access_token 纳入 scoped storage 迁移,yUserId 登录优先并独立持久化 token;轻应用/按钮管理默认改用 access_token 参数名,支持 hash 路由占位符替换与自动追加。
This commit is contained in:
1 parent
8c6d08d6bf
commit
6378cd33bd
10 files changed
+210
-39
No files matched your search
@@ -1,3 +1,7 @@
|
||||
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
|
||||
|
||||
export const EXTERNAL_ACCESS_TOKEN_STORAGE_KEY = "access_token";
|
||||
|
||||
export function normalizeExternalAccessToken(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
const token = String(value).trim();
|
||||
@@ -5,3 +9,53 @@ export function normalizeExternalAccessToken(value: unknown): string {
|
||||
const lowered = token.toLowerCase();
|
||||
return lowered === "null" || lowered === "undefined" ? "" : token;
|
||||
}
|
||||
|
||||
function readTokenFromStoredValue(raw: string | null): string {
|
||||
const token = normalizeExternalAccessToken(raw);
|
||||
if (!token) return "";
|
||||
if (!token.startsWith("{")) return token;
|
||||
try {
|
||||
const parsed = JSON.parse(token);
|
||||
if (!parsed || typeof parsed !== "object") return token;
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
return (
|
||||
normalizeExternalAccessToken(obj.access_token) ||
|
||||
normalizeExternalAccessToken(obj.accessToken) ||
|
||||
normalizeExternalAccessToken(obj.token)
|
||||
);
|
||||
} catch {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
export function readStoredExternalAccessTokenValue(): string {
|
||||
return readTokenFromStoredValue(authStorageGet(EXTERNAL_ACCESS_TOKEN_STORAGE_KEY));
|
||||
}
|
||||
|
||||
export function readExternalAccessTokenValue(userInfo: Record<string, unknown>): string {
|
||||
return normalizeExternalAccessToken(userInfo.access_token) || normalizeExternalAccessToken(userInfo.accessToken);
|
||||
}
|
||||
|
||||
export function readCurrentExternalAccessTokenValue(userInfo: Record<string, unknown>): string {
|
||||
return readStoredExternalAccessTokenValue() || readExternalAccessTokenValue(userInfo);
|
||||
}
|
||||
|
||||
export function writeExternalAccessTokenFields(
|
||||
userInfo: Record<string, unknown>,
|
||||
token: string,
|
||||
): void {
|
||||
userInfo.access_token = token;
|
||||
userInfo.accessToken = token;
|
||||
}
|
||||
|
||||
export function writeStoredExternalAccessToken(token: string): void {
|
||||
const normalized = normalizeExternalAccessToken(token);
|
||||
if (normalized) {
|
||||
authStorageSet(EXTERNAL_ACCESS_TOKEN_STORAGE_KEY, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteExternalAccessTokenFields(userInfo: Record<string, unknown>): void {
|
||||
delete userInfo.access_token;
|
||||
delete userInfo.accessToken;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export function authStorageRemove(key: string): void {
|
||||
* ticket/userId/env/maxkeyOnlineToken/userDetailToken)。用于「开启独立会话」时把当前登录态从
|
||||
* localStorage 快照搬进 sessionStorage,避免切换存储后被登出。
|
||||
*/
|
||||
const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token"];
|
||||
const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token", "access_token"];
|
||||
const PROTECTED_AUTH_PREFIX = "login.";
|
||||
|
||||
function isProtectedAuthKey(key: string): boolean {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { requestCrudService } from "@/service/request-axios-service";
|
||||
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
|
||||
import { normalizeExternalAccessToken } from "@/core/auth/external-access-token";
|
||||
import {
|
||||
deleteExternalAccessTokenFields,
|
||||
normalizeExternalAccessToken,
|
||||
readStoredExternalAccessTokenValue,
|
||||
readExternalAccessTokenValue,
|
||||
writeExternalAccessTokenFields,
|
||||
writeStoredExternalAccessToken,
|
||||
} from "@/core/auth/external-access-token";
|
||||
|
||||
const CONSUMER_URL = import.meta.env.VITE_AIPI_CONSUMER_URL || "";
|
||||
|
||||
@@ -36,6 +43,38 @@ function readStoredYUserId(info: Record<string, unknown>): string {
|
||||
return value == null ? "" : String(value).trim();
|
||||
}
|
||||
|
||||
function normalizeExternalUserId(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
const text = String(value).trim();
|
||||
if (!text) return "";
|
||||
const lowered = text.toLowerCase();
|
||||
return lowered === "null" || lowered === "undefined" ? "" : text;
|
||||
}
|
||||
|
||||
function maskToken(token: string): string {
|
||||
if (!token) return "";
|
||||
if (token.length <= 10) return `${token.slice(0, 2)}***(${token.length})`;
|
||||
return `${token.slice(0, 6)}...${token.slice(-4)}(${token.length})`;
|
||||
}
|
||||
|
||||
function summarizeUserInfo(info: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
keys: Object.keys(info),
|
||||
yUserId: info.yUserId,
|
||||
userId: info.userId,
|
||||
userName: info.userName,
|
||||
gateDepartName: info.gateDepartName,
|
||||
typeName: info.typeName,
|
||||
access_token: maskToken(normalizeExternalAccessToken(info.access_token)),
|
||||
accessToken: maskToken(normalizeExternalAccessToken(info.accessToken)),
|
||||
};
|
||||
}
|
||||
|
||||
function logYUserLogin(step: string, payload: Record<string, unknown>): void {
|
||||
if (typeof console === "undefined") return;
|
||||
console.info(`[yUserId-login] ${step}`, payload);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -95,28 +134,34 @@ function buildExternalUserInfo(
|
||||
}
|
||||
}
|
||||
next.yUserId = yUserId;
|
||||
const externalUserId =
|
||||
normalizeExternalUserId(result.id) ||
|
||||
normalizeExternalUserId(result.userId) ||
|
||||
normalizeExternalUserId(result.user_id) ||
|
||||
yUserId;
|
||||
next.userId = externalUserId;
|
||||
|
||||
const incomingAccessToken = normalizeExternalAccessToken(result.accessToken);
|
||||
const incomingAccessToken = readExternalAccessTokenValue(result);
|
||||
if (incomingAccessToken) {
|
||||
next.accessToken = incomingAccessToken;
|
||||
writeExternalAccessTokenFields(next, incomingAccessToken);
|
||||
return next;
|
||||
}
|
||||
|
||||
const queryAccessToken = normalizeExternalAccessToken(access_token);
|
||||
if (queryAccessToken) {
|
||||
next.accessToken = queryAccessToken;
|
||||
writeExternalAccessTokenFields(next, queryAccessToken);
|
||||
return next;
|
||||
}
|
||||
|
||||
const previousYUserId = readStoredYUserId(previous);
|
||||
const previousAccessToken =
|
||||
previousYUserId === yUserId ? normalizeExternalAccessToken(previous.accessToken) : "";
|
||||
const cachedAccessToken = normalizeExternalAccessToken(cached.accessToken);
|
||||
previousYUserId === yUserId ? readExternalAccessTokenValue(previous) : "";
|
||||
const cachedAccessToken = readExternalAccessTokenValue(cached);
|
||||
const fallbackAccessToken = previousAccessToken || cachedAccessToken;
|
||||
if (fallbackAccessToken) {
|
||||
next.accessToken = fallbackAccessToken;
|
||||
writeExternalAccessTokenFields(next, fallbackAccessToken);
|
||||
} else {
|
||||
delete next.accessToken;
|
||||
deleteExternalAccessTokenFields(next);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
@@ -134,8 +179,16 @@ export async function loginByYUserId(
|
||||
authStorageSet(TOOLS_TOKEN_STORAGE_KEY, token);
|
||||
}
|
||||
|
||||
const requestUrl = `${CONSUMER_URL}/cop/userDetailByYuserId`;
|
||||
logYUserLogin("request", {
|
||||
requestUrl,
|
||||
yUserId,
|
||||
hasToolsToken: Boolean(token),
|
||||
queryAccessToken: maskToken(normalizeExternalAccessToken(access_token)),
|
||||
});
|
||||
|
||||
const res = (await requestCrudService.get(
|
||||
`${CONSUMER_URL}/cop/userDetailByYuserId`,
|
||||
requestUrl,
|
||||
{ yuserId: yUserId },
|
||||
)) as UserDetailResponse;
|
||||
|
||||
@@ -143,12 +196,38 @@ export async function loginByYUserId(
|
||||
// 一并存入路由参数 yUserId,使「应用管理」里 source=yUserId 的路由参数能取到(与
|
||||
// source=userName/gateDepartName/typeName 直接取接口值统一走 userInfo[source])。
|
||||
// 注意:接口返回的 accessToken 优先;登录 URL 的 access_token 只在接口值为空时兜底。
|
||||
// The consumer API may return accessToken. URL access_token is only used as
|
||||
// a fallback when that API token is empty.
|
||||
const result = res.data ?? {};
|
||||
logYUserLogin("response", {
|
||||
keys: Object.keys(result),
|
||||
id: result.id,
|
||||
userId: result.userId,
|
||||
user_id: result.user_id,
|
||||
userName: result.userName,
|
||||
gateDepartName: result.gateDepartName,
|
||||
typeName: result.typeName,
|
||||
access_token: maskToken(normalizeExternalAccessToken(result.access_token)),
|
||||
accessToken: maskToken(normalizeExternalAccessToken(result.accessToken)),
|
||||
});
|
||||
const previous = readExternalUserInfo();
|
||||
const cached = readCachedYUserInfo(yUserId);
|
||||
const next = buildExternalUserInfo(yUserId, result, access_token, previous, cached);
|
||||
const externalUserId = normalizeExternalUserId(next.userId);
|
||||
if (externalUserId) {
|
||||
authStorageSet(EXTERNAL_USER_ID_STORAGE_KEY, externalUserId);
|
||||
}
|
||||
const externalAccessToken = readExternalAccessTokenValue(next);
|
||||
if (externalAccessToken) {
|
||||
writeStoredExternalAccessToken(externalAccessToken);
|
||||
}
|
||||
authStorageSet(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(next));
|
||||
writeCachedYUserInfo(yUserId, next);
|
||||
logYUserLogin("stored", {
|
||||
userId: externalUserId,
|
||||
topLevelAccessToken: maskToken(readStoredExternalAccessTokenValue()),
|
||||
userInfo: summarizeUserInfo(next),
|
||||
});
|
||||
}
|
||||
|
||||
export function persistToolsToken(token: string): void {
|
||||
@@ -156,17 +235,18 @@ export function persistToolsToken(token: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把内网登录路由参数 access_token 合并写入 userInfo.accessToken。
|
||||
* 把内网登录路由参数 access_token 合并写入 userInfo.access_token / userInfo.accessToken。
|
||||
* 据此拼参的有:XK/PG 中任务 iframe(XK 用 accessToken、PG 用 access_token,二者均读
|
||||
* external-links.getAccessToken() = userInfo.accessToken);以及「应用管理」里
|
||||
* source=accessToken 的轻应用路由参数(resolveRouteParamValue 取 userInfo.accessToken)。
|
||||
* external-links.getAccessToken());以及「应用管理」里 source=access_token / accessToken
|
||||
* 的轻应用路由参数(resolveRouteParamValue 统一读两种字段)。
|
||||
* 用合并而非覆盖整对象,避免抹掉 loginByYUserId 从 consumer 拿到的用户信息。
|
||||
*/
|
||||
export function persistAccessToken(token: string): void {
|
||||
const trimmed = normalizeExternalAccessToken(token);
|
||||
if (!trimmed) return;
|
||||
const info = readExternalUserInfo();
|
||||
info.accessToken = trimmed;
|
||||
writeExternalAccessTokenFields(info, trimmed);
|
||||
writeStoredExternalAccessToken(trimmed);
|
||||
authStorageSet(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(info));
|
||||
const yUserId = readStoredYUserId(info);
|
||||
if (yUserId) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Strategy 模块外部 iframe / 嵌入页链接(来自 Vite 环境变量) */
|
||||
|
||||
import { authStorageGet } from "@/core/auth/scoped-storage";
|
||||
import { normalizeExternalAccessToken } from "@/core/auth/external-access-token";
|
||||
import { readCurrentExternalAccessTokenValue } from "@/core/auth/external-access-token";
|
||||
|
||||
function trimTrailingSlash(url: string): string {
|
||||
return url.replace(/\/+$/, "");
|
||||
@@ -44,7 +44,7 @@ function readUserInfo(): Record<string, unknown> {
|
||||
|
||||
/** 当前登录态 accessToken(无则空串)。 */
|
||||
export function getAccessToken(): string {
|
||||
return normalizeExternalAccessToken(readUserInfo().accessToken);
|
||||
return readCurrentExternalAccessTokenValue(readUserInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -89,9 +89,9 @@ const emptyForm = (): FormState => ({
|
||||
});
|
||||
|
||||
const newParam = (): LightAppRouteParam => ({
|
||||
key: "",
|
||||
key: "access_token",
|
||||
paramType: "login",
|
||||
source: "accessToken",
|
||||
source: "access_token",
|
||||
customValue: "",
|
||||
themeValue: "red=red;blue=blue;dark-blue=dark-blue",
|
||||
});
|
||||
@@ -107,7 +107,7 @@ function formFromApp(app: LightApp): FormState {
|
||||
routeParams: app.routeParams.map((p) => ({
|
||||
key: p.key,
|
||||
paramType: p.paramType,
|
||||
source: p.source ?? "accessToken",
|
||||
source: p.source ?? "access_token",
|
||||
customValue: p.customValue ?? "",
|
||||
themeValue: p.themeValue ?? "",
|
||||
})),
|
||||
@@ -118,7 +118,7 @@ function getParamSourceLabel(param: LightAppRouteParam): string {
|
||||
if (param.paramType === "theme") {
|
||||
return `主题参数: ${param.themeValue || ""}`;
|
||||
}
|
||||
const source = param.source || "accessToken";
|
||||
const source = param.source || "access_token";
|
||||
const option = LOGIN_SOURCE_OPTIONS.find((o) => o.value === source);
|
||||
if (source === "other") {
|
||||
return `登录参数: ${param.customValue || ""}`;
|
||||
@@ -977,7 +977,7 @@ export default function LightAppManagementPage() {
|
||||
onChange={(v) =>
|
||||
patchParam(idx, {
|
||||
paramType: v as "login" | "theme",
|
||||
source: v === "login" ? (p.source || "accessToken") : undefined,
|
||||
source: v === "login" ? (p.source || "access_token") : undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -988,7 +988,7 @@ export default function LightAppManagementPage() {
|
||||
<Select
|
||||
className="w-full"
|
||||
size="small"
|
||||
value={p.source || "accessToken"}
|
||||
value={p.source || "access_token"}
|
||||
options={LOGIN_SOURCE_OPTIONS}
|
||||
popupProps={DIALOG_SELECT_POPUP}
|
||||
onChange={(v) => patchParam(idx, { source: v as string })}
|
||||
@@ -1112,7 +1112,7 @@ function cleanParams(params: LightAppRouteParam[]): LightAppRouteParam[] {
|
||||
.map((p) => {
|
||||
const base: LightAppRouteParam = { key: p.key.trim(), paramType: p.paramType };
|
||||
if (p.paramType === "login") {
|
||||
base.source = p.source || "accessToken";
|
||||
base.source = p.source || "access_token";
|
||||
if (base.source === "other") {
|
||||
base.customValue = (p.customValue ?? "").trim();
|
||||
}
|
||||
|
||||
@@ -179,10 +179,10 @@ export default function LoginPage() {
|
||||
auth = await loginByToken(authToken);
|
||||
} else {
|
||||
let deerflowUsername = resolvedUsername;
|
||||
if (onlineTicket) {
|
||||
deerflowUsername = await resolveUsernameByOnlineTicket(onlineTicket);
|
||||
} else if (yUserId) {
|
||||
if (yUserId) {
|
||||
await loginByYUserId(yUserId, toolToken || null, access_token || null);
|
||||
} else if (onlineTicket) {
|
||||
deerflowUsername = await resolveUsernameByOnlineTicket(onlineTicket);
|
||||
} else if (toolToken) {
|
||||
persistToolsToken(toolToken);
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function TaskButtonsPage() {
|
||||
);
|
||||
|
||||
const addLoginParam = (id: string) =>
|
||||
updateLoginParams(id, (ps) => [...ps, { key: "", source: "accessToken", customValue: "" }]);
|
||||
updateLoginParams(id, (ps) => [...ps, { key: "access_token", source: "access_token", customValue: "" }]);
|
||||
|
||||
const patchLoginParam = (id: string, idx: number, patch: Partial<TaskButtonLoginParam>) =>
|
||||
updateLoginParams(id, (ps) => ps.map((p, i) => (i === idx ? { ...p, ...patch } : p)));
|
||||
|
||||
@@ -49,7 +49,7 @@ interface ServerButton {
|
||||
function toLoginParam(p: ServerLoginParam): TaskButtonLoginParam {
|
||||
return {
|
||||
key: p.key ?? "",
|
||||
source: p.source || "accessToken",
|
||||
source: p.source || "access_token",
|
||||
customValue: p.custom_value ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
platformTypeChange,
|
||||
} from './GeneralQA.mock';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { authStorageGet } from '@/core/auth/scoped-storage';
|
||||
|
||||
/**
|
||||
* 通用问答组件
|
||||
@@ -138,7 +139,7 @@ const GeneralQA: React.FC<{
|
||||
const mockSingleRoundStoppedRef = useRef(false);
|
||||
const mockMultiRoundStoppedRef = useRef(false);
|
||||
const chatListRef = useRef<HTMLDivElement | null>(null);
|
||||
const userId = useRef(localStorage.getItem('userId') || '387'); // 用户id
|
||||
const userId = useRef(authStorageGet('userId') || '387'); // 用户id
|
||||
const qaType = useRef('GENERAL'); // 问答类型
|
||||
const chatDataId = useRef(); // 聊天窗口id
|
||||
const compareChatId = useRef(); // 对比对话记录id
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import type { LightApp, LightAppRouteParam } from "@/strategy-components/api/light-apps";
|
||||
import { authStorageGet } from "@/core/auth/scoped-storage";
|
||||
import { normalizeExternalAccessToken } from "@/core/auth/external-access-token";
|
||||
import { readCurrentExternalAccessTokenValue } from "@/core/auth/external-access-token";
|
||||
|
||||
/** iframe 可挂载的父菜单(取自 core/page-layout/sidebar-menu.ts 的现有菜单 id)。 */
|
||||
export const MOUNT_OPTIONS: { label: string; value: string }[] = [
|
||||
@@ -29,7 +29,8 @@ export function mountLabel(value: string | null | undefined): string {
|
||||
* userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。
|
||||
*/
|
||||
export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [
|
||||
{ label: "accessToken", value: "accessToken" },
|
||||
{ label: "access_token", value: "access_token" },
|
||||
{ label: "accessToken(兼容旧配置)", value: "accessToken" },
|
||||
{ label: "yUserId", value: "yUserId" },
|
||||
{ label: "userName", value: "userName" },
|
||||
{ label: "gateDepartName", value: "gateDepartName" },
|
||||
@@ -143,8 +144,8 @@ export function resolveRouteParamValue(
|
||||
}
|
||||
if (param.source) {
|
||||
const value = userInfo[param.source];
|
||||
if (param.source === "accessToken") {
|
||||
return normalizeExternalAccessToken(value);
|
||||
if (param.source === "access_token" || param.source === "accessToken") {
|
||||
return readCurrentExternalAccessTokenValue(userInfo);
|
||||
}
|
||||
return value == null ? "" : String(value);
|
||||
}
|
||||
@@ -157,15 +158,50 @@ export function resolveRouteParamValue(
|
||||
* 兼容 hash 路由地址(如 `https://x#/route`)—— 管理员粘贴完整地址,参数自动追加在末尾,
|
||||
* 落在 hash 路由的 query 上(与现有 XK/PG 页面 `?accessToken=` 拼接方式一致)。
|
||||
*/
|
||||
function appendQueryPairs(url: string, pairs: string[]): string {
|
||||
if (pairs.length === 0) return url;
|
||||
const hashIndex = url.indexOf("#");
|
||||
if (hashIndex >= 0) {
|
||||
const beforeHash = url.slice(0, hashIndex);
|
||||
const hash = url.slice(hashIndex);
|
||||
const sep = hash.includes("?") ? "&" : "?";
|
||||
return `${beforeHash}${hash}${sep}${pairs.join("&")}`;
|
||||
}
|
||||
const sep = url.includes("?") ? "&" : "?";
|
||||
return `${url}${sep}${pairs.join("&")}`;
|
||||
}
|
||||
|
||||
function replaceAccessTokenParam(url: string, accessToken: string): { url: string; replaced: boolean } {
|
||||
if (!accessToken) return { url, replaced: false };
|
||||
let replaced = false;
|
||||
const nextUrl = url.replace(/([?&]access_token)(?:=[^&#]*)?(?=[&#]|$)/g, (match) => {
|
||||
replaced = true;
|
||||
return `${match.split("=")[0]}=${encodeURIComponent(accessToken)}`;
|
||||
});
|
||||
return { url: nextUrl, replaced };
|
||||
}
|
||||
|
||||
export function buildLightAppUrl(
|
||||
app: Pick<LightApp, "url" | "routeParams">,
|
||||
userInfo: Record<string, unknown> = readUserInfo(),
|
||||
theme?: AppThemeColor,
|
||||
): string {
|
||||
const pairs = (app.routeParams ?? [])
|
||||
.filter((p) => p.key)
|
||||
.map((p) => `${encodeURIComponent(p.key)}=${encodeURIComponent(resolveRouteParamValue(p, userInfo, theme))}`);
|
||||
if (pairs.length === 0) return app.url;
|
||||
const sep = app.url.includes("?") ? "&" : "?";
|
||||
return `${app.url}${sep}${pairs.join("&")}`;
|
||||
const routeParams = app.routeParams ?? [];
|
||||
const accessToken = readCurrentExternalAccessTokenValue(userInfo);
|
||||
const tokenUrlResult = replaceAccessTokenParam(app.url, accessToken);
|
||||
const pairs = routeParams
|
||||
.filter((p) => (p.key ?? "").trim())
|
||||
.filter((p) => (p.key ?? "").trim() !== "access_token" || !tokenUrlResult.replaced)
|
||||
.map((p) => {
|
||||
const key = (p.key ?? "").trim();
|
||||
const value = key === "access_token" ? accessToken : resolveRouteParamValue(p, userInfo, theme);
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
});
|
||||
const hasAccessTokenParam =
|
||||
tokenUrlResult.replaced ||
|
||||
routeParams.some((p) => (p.key ?? "").trim() === "access_token");
|
||||
if (!hasAccessTokenParam && accessToken) {
|
||||
pairs.push(`${encodeURIComponent("access_token")}=${encodeURIComponent(accessToken)}`);
|
||||
}
|
||||
return appendQueryPairs(tokenUrlResult.url, pairs);
|
||||
}
|
||||
Reference in new issue
Block a user