feat: 接入管理端跨站单点登录
This commit is contained in:
parent
86b6cbdae4
commit
76b9513d80
@ -41,6 +41,7 @@ pnpm exec playwright test e2e/argon-parity/baseline.spec.ts --project=chromium
|
||||
- `hljs-codeblock` 静态快照需要恢复 Argon `highlightjs-line-numbers` 运行时生成的 `data-line-number` 与 `.hljs-ln-n::before`,否则行号列会坍塌成 0px。
|
||||
- 左栏 overview sticky/relative 切换用 no-headroom 回归用例固定,解除 fixed 后不得重放卡片入场动画或产生缩放闪烁。
|
||||
- 左栏文章目录/站点概览切换必须保留 Bootstrap tab fade 节奏,采用 active/show 分帧保持 Argon 手感;回归用例需要断言切换中 opacity 处于 0 到 1 之间,而不是只检查最终显隐。
|
||||
- 左栏“管理”始终使用跨站 KT Admin SSO:生产默认跳到 `https://admin.kwitsukasa.top/#/auth/login?sso=1&redirect=%2Fblog%2Farticle`,本地默认使用 `http://localhost:5999/`,可通过 `VITE_KT_ADMIN_BASE_URL` 覆盖 Admin 基址。旧主题接口返回的 `/wp-admin/` 会在最终侧栏菜单中迁移到该入口;Blog 不跨域读取 Cookie,也不在 URL 中传递 token。
|
||||
- Argon motion、滚动几何、延迟、RAF 调度、DOM id/selector、hash anchor 和跨组件 ref 必须从 `src/factories/blogAnimationFactory.ts` 与 `src/factories/blogDomFactory.ts` 取值;组件和 Hook 不再散写行为层 id、裸 `requestAnimationFrame`、裸 `setTimeout` 或重复 timing。
|
||||
- Live2D 使用旧 WordPress 站点同款 Cubism2 MOC 资源,`BlogLive2D` 与 `src/components/blog/live2d/runtime/*` 负责 canvas、Pio/Tia catalog、MOC metadata、选择缓存、WebGL 渲染、真实 MTN 动作队列、全页面有界视线、canvas-local 触摸和模型/服装直达切换。点击命中保留原 `live2d.min.js` 的 canvas-local width 分母坐标;桌面鼠标视线以模型中心为零点,按中心到对应 viewport 边缘的距离连续归一化,使 canvas 外近/中/远距离仍可区分。运行时保持源码的 `loadParam -> motion/eye blink -> saveParam -> pointer/sine/breath -> update` 参数生命周期,并直接导入类型化、语义化的 TS Core;不包含 compatibility/global installer,也不得 append 原始 `live2d.min.js`。
|
||||
- Cubism2 反混淆只以 `public/live2d/wordpress-moc/live2d.min.js` 为源码证据,并严格按“函数名恢复 -> 调用逻辑理清 -> 拆分压缩耦合 -> 变量名恢复 -> runtime 全覆盖”推进;生产运行时不得加载该 min.js。权威进度维护在 `docs/blog-live2d-cubism2-minjs-deobfuscation-ledger.md`,稳定决策位于 `docs/live2d-deobfuscation/`。当前主线已闭合:627/627 函数、1411/1411 调用、567 owned/60 omitted、578 参数/1169 局部/53 最终标识符;runtime 分类为 546 exact、14 intentional bug fix、7 defensive extension、60 omitted-unreachable。生产 Core 严格等于 38 个 owner module + 5 个 sealed support module、0 orphan,并直接由 `runtimeCore.ts`/renderer 消费;禁止重新引入 compatibility、legacy globals、alias、映射层或原始 min.js 加载。
|
||||
|
||||
@ -1064,7 +1064,10 @@ function createSidebarMenuSurfaceTest() {
|
||||
await expect(homeIcon).toHaveCSS('display', 'inline-block');
|
||||
await expect(homeIcon).toHaveCSS('width', '15px');
|
||||
await expect(homeIcon).toHaveCSS('margin-right', '8px');
|
||||
await expect(adminLink).toHaveAttribute('href', 'http://blog.kwitsukasa.top/wp-admin/');
|
||||
await expect(adminLink).toHaveAttribute(
|
||||
'href',
|
||||
'http://localhost:5999/#/auth/login?sso=1&redirect=%2Fblog%2Farticle',
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
42
src/__tests__/BlogAdminSso.spec.ts
Normal file
42
src/__tests__/BlogAdminSso.spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
BLOG_ADMIN_MANAGEMENT_PATH,
|
||||
buildBlogAdminSsoUrl,
|
||||
isLegacyWordpressAdminHref,
|
||||
resolveBlogAdminBaseUrl,
|
||||
} from '@/factories/blogAdminSsoFactory'
|
||||
|
||||
describe('Blog Admin SSO URL factory', () => {
|
||||
it('builds a cross-site Admin login bootstrap without exposing a token', () => {
|
||||
const href = buildBlogAdminSsoUrl('https://admin.kwitsukasa.top/')
|
||||
const url = new URL(href)
|
||||
const hashUrl = new URL(url.hash.slice(1), url.origin)
|
||||
|
||||
expect(url.origin).toBe('https://admin.kwitsukasa.top')
|
||||
expect(hashUrl.pathname).toBe('/auth/login')
|
||||
expect(hashUrl.searchParams.get('sso')).toBe('1')
|
||||
expect(hashUrl.searchParams.get('redirect')).toBe(BLOG_ADMIN_MANAGEMENT_PATH)
|
||||
expect(href).not.toMatch(/token|credential/i)
|
||||
})
|
||||
|
||||
it('uses the configured Admin origin and safe local/production fallbacks', () => {
|
||||
expect(
|
||||
resolveBlogAdminBaseUrl({
|
||||
PROD: true,
|
||||
VITE_KT_ADMIN_BASE_URL: 'https://admin.example.com/base/',
|
||||
}),
|
||||
).toBe('https://admin.example.com/base/')
|
||||
expect(resolveBlogAdminBaseUrl({ PROD: true })).toBe('https://admin.kwitsukasa.top/')
|
||||
expect(resolveBlogAdminBaseUrl({ PROD: false })).toBe('http://localhost:5999/')
|
||||
})
|
||||
|
||||
it('identifies only legacy WordPress management destinations', () => {
|
||||
expect(isLegacyWordpressAdminHref('http://blog.kwitsukasa.top/wp-admin/')).toBe(true)
|
||||
expect(isLegacyWordpressAdminHref('/wp-admin')).toBe(true)
|
||||
expect(isLegacyWordpressAdminHref('https://blog.kwitsukasa.top/post/wp-admin-migration')).toBe(
|
||||
false,
|
||||
)
|
||||
expect(isLegacyWordpressAdminHref('https://example.com/manage')).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,7 @@
|
||||
import { nextTick } from 'vue';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { buildBlogAdminSsoUrl } from '@/factories/blogAdminSsoFactory';
|
||||
import { useBlogTheme } from '@/hooks/useBlogTheme';
|
||||
|
||||
vi.mock('antdv-next', () => ({
|
||||
@ -120,7 +121,7 @@ describe('useBlogTheme', () => {
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
href: 'http://blog.kwitsukasa.top/wp-admin/',
|
||||
href: buildBlogAdminSsoUrl(),
|
||||
icon: 'fa-user',
|
||||
label: '管理',
|
||||
},
|
||||
@ -197,6 +198,45 @@ describe('useBlogTheme', () => {
|
||||
expect(siteConfig.value.headerMenuVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('migrates same-site WordPress management without changing unrelated sidebar links', async () => {
|
||||
const { applyWordpressThemeConfig, siteConfig } = useBlogTheme();
|
||||
|
||||
applyWordpressThemeConfig({
|
||||
sidebarMenu: [
|
||||
{
|
||||
href: 'https://blog.kwitsukasa.top/wp-admin/',
|
||||
icon: 'fa-user',
|
||||
label: '管理',
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
href: 'https://github.com/KwiTsukasa',
|
||||
icon: 'fa-github',
|
||||
label: 'GitHub',
|
||||
},
|
||||
],
|
||||
site: {
|
||||
home: 'https://blog.kwitsukasa.top',
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(siteConfig.value.sidebarMenu).toEqual([
|
||||
{
|
||||
external: true,
|
||||
href: buildBlogAdminSsoUrl(),
|
||||
icon: 'fa-user',
|
||||
label: '管理',
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
href: 'https://github.com/KwiTsukasa',
|
||||
icon: 'fa-github',
|
||||
label: 'GitHub',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('maps legacy Argon placeholder image config back to previous blog static assets', async () => {
|
||||
const { applyWordpressThemeConfig, siteConfig } = useBlogTheme();
|
||||
|
||||
|
||||
60
src/factories/blogAdminSsoFactory.ts
Normal file
60
src/factories/blogAdminSsoFactory.ts
Normal file
@ -0,0 +1,60 @@
|
||||
interface BlogAdminEnvironment {
|
||||
PROD?: boolean
|
||||
VITE_KT_ADMIN_BASE_URL?: string
|
||||
}
|
||||
|
||||
const LOCAL_ADMIN_BASE_URL = 'http://localhost:5999/'
|
||||
const PRODUCTION_ADMIN_BASE_URL = 'https://admin.kwitsukasa.top/'
|
||||
const ADMIN_SSO_LOGIN_PATH = '/auth/login'
|
||||
|
||||
export const BLOG_ADMIN_MANAGEMENT_PATH = '/blog/article'
|
||||
|
||||
/**
|
||||
* Resolves the KT Admin origin used by the Blog management entry.
|
||||
* @param env Vite environment values; a configured HTTP(S) URL wins over the local/production default.
|
||||
* @returns Absolute KT Admin base URL.
|
||||
*/
|
||||
export function resolveBlogAdminBaseUrl(env: BlogAdminEnvironment = import.meta.env) {
|
||||
const configured = env.VITE_KT_ADMIN_BASE_URL?.trim()
|
||||
const fallback = env.PROD ? PRODUCTION_ADMIN_BASE_URL : LOCAL_ADMIN_BASE_URL
|
||||
|
||||
if (!configured) return fallback
|
||||
|
||||
try {
|
||||
const url = new URL(configured)
|
||||
return /^https?:$/.test(url.protocol) ? url.toString() : fallback
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the cross-site Admin SSO bootstrap URL for Blog article management.
|
||||
* @param adminBaseUrl Absolute KT Admin base URL; defaults to the Vite environment contract.
|
||||
* @returns Token-free top-level navigation URL targeting the Admin SSO bootstrap.
|
||||
*/
|
||||
export function buildBlogAdminSsoUrl(adminBaseUrl = resolveBlogAdminBaseUrl()) {
|
||||
const url = new URL(adminBaseUrl)
|
||||
const query = new URLSearchParams({
|
||||
sso: '1',
|
||||
redirect: BLOG_ADMIN_MANAGEMENT_PATH,
|
||||
})
|
||||
|
||||
url.search = ''
|
||||
url.hash = `${ADMIN_SSO_LOGIN_PATH}?${query.toString()}`
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the legacy WordPress dashboard destination that remote Argon theme data may still expose.
|
||||
* @param href Normalized sidebar destination, absolute or site-relative.
|
||||
* @returns Whether the destination is the WordPress administration root or one of its descendants.
|
||||
*/
|
||||
export function isLegacyWordpressAdminHref(href: string) {
|
||||
try {
|
||||
const pathname = new URL(href, 'https://blog.invalid/').pathname.replace(/\/+$/g, '')
|
||||
return pathname === '/wp-admin' || pathname.startsWith('/wp-admin/')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,10 @@ import {
|
||||
unwrapBlogCssImage,
|
||||
} from '@/data/blogStaticAssets';
|
||||
import { createBlogMotionCssVariables } from '@/factories/blogAnimationFactory';
|
||||
import {
|
||||
buildBlogAdminSsoUrl,
|
||||
isLegacyWordpressAdminHref,
|
||||
} from '@/factories/blogAdminSsoFactory';
|
||||
import { BLOG_META_NAMES, blogDomId, blogDomSelector, blogMetaSelector } from '@/factories/blogDomFactory';
|
||||
|
||||
export type BlogThemeMode = 'dark' | 'light';
|
||||
@ -122,7 +126,7 @@ const ARGON_DEFAULT_AUTHOR_AVATAR = PREVIOUS_BLOG_AUTHOR_AVATAR;
|
||||
const defaultHeaderMenu: BlogThemeMenuItem[] = [];
|
||||
const defaultSidebarMenu: BlogThemeMenuItem[] = [
|
||||
{ href: '/', icon: 'fa-home', label: '首页' },
|
||||
{ external: true, href: 'http://blog.kwitsukasa.top/wp-admin/', icon: 'fa-user', label: '管理' },
|
||||
{ external: true, href: buildBlogAdminSsoUrl(), icon: 'fa-user', label: '管理' },
|
||||
];
|
||||
|
||||
const defaultPreferences: BlogThemePreferences = {
|
||||
@ -336,7 +340,7 @@ function applyWordpressThemeConfig(config: WordpressArgonThemeConfig) {
|
||||
runtimeConfig.siteTitle = config.site?.title || runtimeConfig.siteTitle;
|
||||
runtimeConfig.siteUrl = config.site?.url || runtimeConfig.siteUrl;
|
||||
if (Array.isArray(config.sidebarMenu)) {
|
||||
runtimeConfig.sidebarMenu = normalizeMenuItems(
|
||||
runtimeConfig.sidebarMenu = normalizeSidebarMenuItems(
|
||||
config.sidebarMenu,
|
||||
config.site?.home || config.site?.url || runtimeConfig.siteHome || runtimeConfig.siteUrl,
|
||||
);
|
||||
@ -1014,6 +1018,27 @@ function normalizeMenuItems(items: BlogThemeMenuItem[], siteHome = ''): BlogThem
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes sidebar items and migrates the legacy WordPress dashboard entry to KT Admin SSO.
|
||||
* @param items Raw sidebar items supplied by the Blog theme API.
|
||||
* @param siteHome Public Blog home URL used to collapse same-site links into RouterLink paths.
|
||||
* @returns Sidebar items with only the legacy management destination replaced by an external SSO URL.
|
||||
*/
|
||||
function normalizeSidebarMenuItems(
|
||||
items: BlogThemeMenuItem[],
|
||||
siteHome = '',
|
||||
): BlogThemeMenuItem[] {
|
||||
return normalizeMenuItems(items, siteHome).map((item) =>
|
||||
isLegacyWordpressAdminHref(item.href)
|
||||
? {
|
||||
...item,
|
||||
external: true,
|
||||
href: buildBlogAdminSsoUrl(),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeMenuHref(href: string, siteHome = '') {
|
||||
if (!href) return '';
|
||||
const normalizedSiteHome = siteHome.replace(/\/+$/g, '');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user