Files
memind/src/App.tsx
T
john f818dd0307 Add WeChat in-app JSAPI recharge for g2.tkmind.cn.
Use JSAPI with bound openid inside WeChat instead of QR long-press, and include related auth redirect and admin UX fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:20:04 +08:00

221 lines
6.9 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { checkAuth, logout, setUnauthorizedHandler } from './api/client';
import { AdminLayout } from './admin/AdminLayout';
import { BillingPage } from './admin/pages/BillingPage';
import { CapabilitiesPage } from './admin/pages/CapabilitiesPage';
import { DashboardPage } from './admin/pages/DashboardPage';
import { PoliciesPage } from './admin/pages/PoliciesPage';
import { ProvidersPage } from './admin/pages/ProvidersPage';
import { SkillsPage } from './admin/pages/SkillsPage';
import { UserDetailPage } from './admin/pages/UserDetailPage';
import { UsersPage } from './admin/pages/UsersPage';
import { AuthView } from './components/AuthView';
import { ChatView } from './components/ChatView';
import { MindSpaceView } from './components/MindSpaceView';
import { ChatProvider } from './context/ChatProvider';
import { PREVIEW_USER } from './dev/mindspacePreviewData';
import { MindSpaceRoute } from './routes/MindSpaceRoute';
import { RequireAdmin } from './routes/RequireAdmin';
import type { CapabilityMap, PortalUser } from './types';
function isMindSpacePreview() {
return (
import.meta.env.DEV &&
new URLSearchParams(window.location.search).get('preview') === 'mindspace'
);
}
function MindSpaceAuthGate({
user,
onLogout,
}: {
user: PortalUser | null;
onLogout: () => void;
}) {
const location = useLocation();
if (!user) {
const params = new URLSearchParams();
params.set('return_to', `${location.pathname}${location.search}`);
const utmSource = new URLSearchParams(location.search).get('utm_source');
if (utmSource) params.set('utm_source', utmSource);
return <Navigate to={`/?${params.toString()}`} replace />;
}
return <MindSpaceRoute user={user} onLogout={onLogout} />;
}
function AuthenticatedApp({
user,
capabilities,
grantedSkills,
onUserUpdate,
onLogout,
}: {
user: PortalUser | null;
capabilities?: CapabilityMap;
grantedSkills?: string[];
onUserUpdate: (user: PortalUser) => void;
onLogout: () => void;
}) {
const navigate = useNavigate();
const handleLogout = () => {
void logout().finally(() => {
onLogout();
navigate('/', { replace: true });
});
};
const chatElement = (
<ChatView
user={user}
capabilities={capabilities}
grantedSkills={grantedSkills}
onUserUpdate={onUserUpdate}
onOpenSpace={
user
? (target) => {
if (target?.pageId) {
navigate(`/space/page/${target.pageId}`);
return;
}
if (target?.categoryCode) {
navigate(`/space?category=${target.categoryCode}`);
return;
}
navigate('/space');
}
: undefined
}
onOpenPage={user ? (pageId) => navigate(`/space/page/${pageId}`) : undefined}
onOpenAdmin={user?.role === 'admin' ? () => navigate('/admin') : undefined}
onLogout={handleLogout}
/>
);
return (
<ChatProvider user={user} capabilities={capabilities} onUserUpdate={onUserUpdate}>
<Routes>
<Route
path="/admin"
element={
<RequireAdmin user={user}>
{user ? <AdminLayout user={user} onLogout={handleLogout} /> : null}
</RequireAdmin>
}
>
<Route index element={<DashboardPage />} />
<Route path="users" element={<UsersPage />} />
<Route path="users/:userId" element={<UserDetailPage />} />
<Route path="billing/*" element={<BillingPage />} />
<Route path="capabilities" element={<CapabilitiesPage />} />
<Route path="skills" element={<SkillsPage />} />
<Route path="policies" element={<PoliciesPage />} />
<Route path="providers" element={<ProvidersPage />} />
<Route path="*" element={<Navigate to="/admin" replace />} />
</Route>
<Route
path="/space/*"
element={<MindSpaceAuthGate user={user} onLogout={handleLogout} />}
/>
<Route path="/" element={chatElement} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</ChatProvider>
);
}
export function App() {
const mindSpacePreview = isMindSpacePreview();
const navigate = useNavigate();
const [authed, setAuthed] = useState<boolean | null>(null);
const [user, setUser] = useState<PortalUser | null>(null);
const [capabilities, setCapabilities] = useState<CapabilityMap | undefined>();
const [grantedSkills, setGrantedSkills] = useState<string[] | undefined>();
const [legacyMode, setLegacyMode] = useState(false);
useEffect(() => {
if (mindSpacePreview) return;
setUnauthorizedHandler(() => {
setAuthed(false);
setUser(null);
if (window.location.pathname !== '/') {
navigate('/', { replace: true });
}
});
void checkAuth().then((status) => {
setLegacyMode(status.mode === 'legacy');
setAuthed(status.authenticated);
setUser(status.user ?? null);
setCapabilities(status.capabilities);
setGrantedSkills(status.grantedSkills);
});
return () => setUnauthorizedHandler(null);
}, [mindSpacePreview, navigate]);
if (mindSpacePreview) {
return (
<ChatProvider user={PREVIEW_USER}>
<MindSpaceView
previewMode
user={PREVIEW_USER}
onBack={() => {
window.location.href = '/';
}}
onLogout={() => {
window.location.href = '/';
}}
/>
</ChatProvider>
);
}
if (authed === null) {
return <div className="app-loading">正在验证登录状态…</div>;
}
if (!authed) {
return (
<AuthView
legacyMode={legacyMode}
onAuth={(nextUser, nextCapabilities, nextSkills) => {
const params = new URLSearchParams(window.location.search);
const returnTo =
params.get('return_to') ??
(window.location.pathname.startsWith('/admin')
? `${window.location.pathname}${window.location.search}`
: null);
if (returnTo) {
try {
const url = new URL(returnTo, window.location.origin);
if (url.origin === window.location.origin) {
window.location.href = url.toString();
return;
}
} catch {
// ignore invalid URLs
}
}
setAuthed(true);
setUser(nextUser ?? null);
setCapabilities(nextCapabilities);
setGrantedSkills(nextSkills);
}}
/>
);
}
return (
<AuthenticatedApp
user={user}
capabilities={capabilities}
grantedSkills={grantedSkills}
onUserUpdate={setUser}
onLogout={() => {
setAuthed(false);
setUser(null);
}}
/>
);
}