From dd3fed5b14c935e8cf42e27d68ffad5500a1842c Mon Sep 17 00:00:00 2001 From: john Date: Fri, 24 Jul 2026 17:12:31 +0800 Subject: [PATCH] feat: add Rybbit admin SSO entry --- .env.example | 5 ++++ server/app.mjs | 15 +++++++++++ src/admin/pages/AnalyticsConfigPage.tsx | 35 ++++++++++++++++++++----- src/api/client.ts | 5 ++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 70a760e..fdb6f50 100644 --- a/.env.example +++ b/.env.example @@ -42,5 +42,10 @@ VITE_MAIN_APP_URL=https://h5.tkmind.cn MEMIND_UMAMI_SSO_SECRET=replace-with-the-same-random-secret-used-by-umami UMAMI_SSO_USERNAME=admin +# Rybbit 仅允许从 MemindAdm 单点登录进入(必须与 105 Rybbit 一致) +MEMIND_RYBBIT_SSO_SECRET=replace-with-the-same-random-secret-used-by-rybbit +RYBBIT_SSO_EMAIL=admin@tkmind.cn +RYBBIT_URL=https://rybbit.tkmind.cn + # Plaza 帖子预览链接 # VITE_PLAZA_BASE=https://plaza.tkmind.cn diff --git a/server/app.mjs b/server/app.mjs index 32dd843..2169717 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -232,6 +232,21 @@ export function createAdminApp(services) { res.json({ url: `${baseUrl}/auth/memind?ticket=${encoded}.${signature}` }); }); + adminApi.get('/analytics/rybbit-sso', requireAdmin, async (_req, res) => { + const sharedSecret = process.env.MEMIND_RYBBIT_SSO_SECRET?.trim(); + if (!sharedSecret) return res.status(503).json({ message: '未配置 Rybbit 单点登录密钥' }); + const email = process.env.RYBBIT_SSO_EMAIL?.trim().toLowerCase(); + if (!email) return res.status(503).json({ message: '未配置 Rybbit 单点登录账号' }); + const baseUrl = String(process.env.RYBBIT_URL || 'https://rybbit.tkmind.cn').replace(/\/$/, ''); + const encoded = Buffer.from(JSON.stringify({ + email, + exp: Math.floor(Date.now() / 1000) + 60, + nonce: crypto.randomUUID(), + })).toString('base64url'); + const signature = crypto.createHmac('sha256', sharedSecret).update(encoded).digest('base64url'); + res.json({ url: `${baseUrl}/api/auth/memind?ticket=${encoded}.${signature}` }); + }); + adminApi.get('/users', requireAdmin, async (req, res) => { const result = await userAuth.listUsers({ page: Number(req.query.page) || 1, diff --git a/src/admin/pages/AnalyticsConfigPage.tsx b/src/admin/pages/AnalyticsConfigPage.tsx index a0633f4..c81a050 100644 --- a/src/admin/pages/AnalyticsConfigPage.tsx +++ b/src/admin/pages/AnalyticsConfigPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState, type FormEvent } from 'react'; -import { getMindSpaceAdminConfig, getUmamiSsoUrl, updateMindSpaceAdminConfig } from '../../api/client'; +import { getMindSpaceAdminConfig, getRybbitSsoUrl, getUmamiSsoUrl, updateMindSpaceAdminConfig } from '../../api/client'; const initial = { enabled: false, @@ -16,7 +16,8 @@ export function AnalyticsConfigPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [message, setMessage] = useState(null); - const [opening, setOpening] = useState(false); + const [openingUmami, setOpeningUmami] = useState(false); + const [openingRybbit, setOpeningRybbit] = useState(false); const load = useCallback(async () => { setLoading(true); @@ -52,23 +53,43 @@ export function AnalyticsConfigPage() { }; const openUmami = async () => { - setOpening(true); + setOpeningUmami(true); setError(null); try { window.location.assign(await getUmamiSsoUrl()); } catch (err) { setError(err instanceof Error ? err.message : '无法打开 Umami'); - setOpening(false); + setOpeningUmami(false); + } + }; + + const openRybbit = async () => { + setOpeningRybbit(true); + setError(null); + try { + window.location.assign(await getRybbitSsoUrl()); + } catch (err) { + setError(err instanceof Error ? err.message : '无法打开 Rybbit'); + setOpeningRybbit(false); } }; return
-

Analytics 配置

配置本地 Memind 生成页面的 Umami 统计。默认只连接本机 Umami,不涉及生产环境。

+

Analytics 配置

进入统计后台,或配置 Memind 生成页面的 Umami 统计。

{error &&

{error}

} {message &&

{message}

}
-

本地 Umami

-
+

Rybbit 行为分析

+

查看页面访问、用户会话、停留时间、点击和会话回放等行为明细。

+
+ +
+
+
+

Umami

+
diff --git a/src/api/client.ts b/src/api/client.ts index f77a8ef..ec5e65c 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -327,6 +327,11 @@ export async function getUmamiSsoUrl(): Promise { return result.url; } +export async function getRybbitSsoUrl(): Promise { + const result = await portalFetch<{ url: string }>('/admin-api/analytics/rybbit-sso'); + return result.url; +} + export async function updateMindSpaceAdminConfig( payload: Partial & { analytics?: MindSpaceAnalyticsUpdate }, ): Promise {