fix(frontend): preserve iframe theme across login
The iframe login used a fixed queryless redirect and called setTheme, which both dropped the iframe theme and persisted it into the normal site's shared theme storage.
This commit is contained in:
1 parent
59f2f56124
commit
75aad4eef6
6 files changed
+187
-30
No files matched your search
@@ -1,3 +1,7 @@
|
||||
import { resolveEmbedThemeMode } from "./theme-mode.mjs";
|
||||
|
||||
export { resolveEmbedThemeMode } from "./theme-mode.mjs";
|
||||
|
||||
/**
|
||||
* 「外部系统 iframe 套用深链」相关的**按标签页**会话开关。承载三个相互独立的能力,多由 URL
|
||||
* 路由参数开启(独立会话 `isolate` 另可由「外观」设置里的开关在运行时切换,见 `setIsolatedSession`),
|
||||
@@ -85,16 +89,6 @@ export function readIsolateParam(search: string): FlagParam {
|
||||
return readFlagParam(search, "isolate");
|
||||
}
|
||||
|
||||
/**
|
||||
* 把外部传入的主题色映射成离散明/暗。与 `resolveThemeFromQuery` 同源约定(dark-blue → 深色),
|
||||
* 另外宽松接受 `dark` / 中文「深色/深蓝」直传;其余(含空)一律浅色。
|
||||
*/
|
||||
export function resolveEmbedThemeMode(raw: string | null | undefined): EmbedTheme {
|
||||
const v = (raw ?? "").trim().toLowerCase();
|
||||
if (v === "dark" || v === "dark-blue" || v === "深色" || v === "深蓝") return "dark";
|
||||
return "light";
|
||||
}
|
||||
|
||||
/** 取 URL query 里的 `theme` 值(embed 主题来源)。 */
|
||||
export function readEmbedThemeParam(search: string): string {
|
||||
return (toSearchParams(search).get("theme") ?? "").trim();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @typedef {"light" | "dark"} EmbedThemeMode
|
||||
*/
|
||||
|
||||
/**
|
||||
* Map external theme names to the two themes supported by iframe surfaces.
|
||||
*
|
||||
* @param {string | null | undefined} rawTheme
|
||||
* @returns {EmbedThemeMode}
|
||||
*/
|
||||
export function resolveEmbedThemeMode(rawTheme) {
|
||||
const theme = (rawTheme ?? "").trim().toLowerCase();
|
||||
return theme === "dark" ||
|
||||
theme === "dark-blue" ||
|
||||
theme === "深色" ||
|
||||
theme === "深蓝"
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildChatsIframeRedirectPath,
|
||||
buildChatsIframeThreadPath,
|
||||
isChatsIframeWorkspacePath,
|
||||
resolvePostLoginRedirectPath,
|
||||
resolveForcedThemeForPath,
|
||||
} from "./chats-iframe-route.ts";
|
||||
|
||||
@@ -48,3 +51,73 @@ test("honors the dark-blue theme query on chats_iframe routes", () => {
|
||||
"light",
|
||||
);
|
||||
});
|
||||
|
||||
test("forces the iframe theme on the login route without changing normal login", () => {
|
||||
assert.equal(
|
||||
resolveForcedThemeForPath(
|
||||
"/login",
|
||||
"?from=iframe&theme=dark-blue",
|
||||
),
|
||||
"dark",
|
||||
);
|
||||
assert.equal(
|
||||
resolveForcedThemeForPath(
|
||||
"/login/guest",
|
||||
"?from=iframe&theme=dark-blue",
|
||||
),
|
||||
"dark",
|
||||
);
|
||||
assert.equal(
|
||||
resolveForcedThemeForPath("/login", "?theme=dark-blue"),
|
||||
undefined,
|
||||
);
|
||||
assert.equal(
|
||||
resolveForcedThemeForPath(
|
||||
"/page/workspace/chats/new",
|
||||
"?from=iframe&theme=dark-blue",
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves the iframe theme in the post-login redirect", () => {
|
||||
assert.equal(
|
||||
buildChatsIframeRedirectPath("?from=iframe&theme=dark-blue"),
|
||||
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
|
||||
);
|
||||
assert.equal(
|
||||
buildChatsIframeRedirectPath("?from=iframe"),
|
||||
"/page/workspace/chats_iframe/new?from=iframe",
|
||||
);
|
||||
assert.equal(
|
||||
buildChatsIframeRedirectPath(
|
||||
"?from=iframe&theme=dark-blue&ticket=secret&authToken=secret",
|
||||
),
|
||||
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps the iframe redirect ahead of login-method fallbacks", () => {
|
||||
assert.equal(
|
||||
resolvePostLoginRedirectPath(
|
||||
"?from=iframe&theme=dark-blue&yUserId=user-1",
|
||||
"/page/workspace/chats/new",
|
||||
),
|
||||
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
|
||||
);
|
||||
assert.equal(
|
||||
resolvePostLoginRedirectPath("?yUserId=user-1", "/page/workspace/chats/new"),
|
||||
"/page/workspace/chats/new",
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves iframe theme when a new chat becomes a thread", () => {
|
||||
assert.equal(
|
||||
buildChatsIframeThreadPath(
|
||||
"/page/workspace",
|
||||
"thread-123",
|
||||
"?from=iframe&theme=dark-blue&ticket=secret",
|
||||
),
|
||||
"/page/workspace/chats_iframe/thread-123?from=iframe&theme=dark-blue",
|
||||
);
|
||||
});
|
||||
@@ -1,8 +1,60 @@
|
||||
import { resolveEmbedThemeMode } from "@/core/embed/embed-session";
|
||||
import { resolveEmbedThemeMode } from "../embed/theme-mode.mjs";
|
||||
|
||||
const CHATS_IFRAME_PATH = "/page/workspace/chats_iframe/new";
|
||||
const IFRAME_LOGIN_SOURCE = "iframe";
|
||||
|
||||
function toSearchParams(search: string): URLSearchParams {
|
||||
return new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
|
||||
}
|
||||
|
||||
export function isChatsIframeLogin(search: string): boolean {
|
||||
return toSearchParams(search).get("from") === IFRAME_LOGIN_SOURCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve only the presentation parameters consumed by the iframe workspace.
|
||||
* Login credentials must never be copied into the post-login address bar.
|
||||
*/
|
||||
export function buildChatsIframeRedirectPath(search: string): string {
|
||||
const source = toSearchParams(search);
|
||||
const target = new URLSearchParams();
|
||||
|
||||
if (isChatsIframeLogin(search)) {
|
||||
target.set("from", IFRAME_LOGIN_SOURCE);
|
||||
}
|
||||
|
||||
const theme = source.get("theme")?.trim();
|
||||
if (theme) {
|
||||
target.set("theme", theme);
|
||||
}
|
||||
|
||||
const query = target.toString();
|
||||
return query ? `${CHATS_IFRAME_PATH}?${query}` : CHATS_IFRAME_PATH;
|
||||
}
|
||||
|
||||
export function buildChatsIframeThreadPath(
|
||||
routeBase: string,
|
||||
threadId: string,
|
||||
search: string,
|
||||
): string {
|
||||
const base = routeBase.endsWith("/") ? routeBase.slice(0, -1) : routeBase;
|
||||
const query = buildChatsIframeRedirectPath(search).split("?")[1];
|
||||
const path = `${base}/chats_iframe/${encodeURIComponent(threadId)}`;
|
||||
return query ? `${path}?${query}` : path;
|
||||
}
|
||||
|
||||
export function resolvePostLoginRedirectPath(
|
||||
search: string,
|
||||
fallbackPath: string,
|
||||
): string {
|
||||
return isChatsIframeLogin(search)
|
||||
? buildChatsIframeRedirectPath(search)
|
||||
: fallbackPath;
|
||||
}
|
||||
|
||||
export function isChatsIframeWorkspacePath(pathname: string): boolean {
|
||||
return (
|
||||
pathname === "/page/workspace/chats_iframe/new" ||
|
||||
pathname === CHATS_IFRAME_PATH ||
|
||||
pathname.startsWith("/page/workspace/chats_iframe/")
|
||||
);
|
||||
}
|
||||
@@ -15,9 +67,12 @@ export function resolveForcedThemeForPath(
|
||||
return "light";
|
||||
}
|
||||
|
||||
if (isChatsIframeWorkspacePath(pathname)) {
|
||||
const normalizedSearch = search.startsWith("?") ? search : `?${search}`;
|
||||
const rawTheme = new URLSearchParams(normalizedSearch).get("theme")?.trim();
|
||||
const isLoginPath = pathname === "/login" || pathname.startsWith("/login/");
|
||||
if (
|
||||
isChatsIframeWorkspacePath(pathname) ||
|
||||
(isLoginPath && isChatsIframeLogin(search))
|
||||
) {
|
||||
const rawTheme = toSearchParams(search).get("theme")?.trim();
|
||||
return rawTheme ? resolveEmbedThemeMode(rawTheme) : "light";
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user