Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
|
||||
const links = [
|
||||
{ to: '/admin', label: '概览', end: true },
|
||||
{ to: '/admin/users', label: '用户管理' },
|
||||
{ to: '/admin/llm', label: 'LLM 配置' },
|
||||
{ to: '/admin/billing', label: '账单记录' },
|
||||
];
|
||||
|
||||
export function AdminLayout() {
|
||||
return (
|
||||
<div className="layout">
|
||||
<header>
|
||||
<h1>超级管理后台</h1>
|
||||
<p style={{ color: '#68716c' }}>用户、计费与 LLM 配置</p>
|
||||
</header>
|
||||
<nav className="nav">
|
||||
<NavLink
|
||||
to="/"
|
||||
style={{ opacity: 0.6 }}
|
||||
>
|
||||
← 运营后台
|
||||
</NavLink>
|
||||
{links.map((link) => (
|
||||
<NavLink
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
end={link.end}
|
||||
className={({ isActive }) => (isActive ? 'active' : undefined)}
|
||||
>
|
||||
{link.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { useAuth } from '../lib/auth';
|
||||
|
||||
const links = [
|
||||
{ to: '/', label: '审核队列' },
|
||||
const opsLinks = [
|
||||
{ to: '/', label: '审核队列', end: true },
|
||||
{ to: '/reports', label: '举报处理' },
|
||||
{ to: '/featured', label: '精选管理' },
|
||||
{ to: '/creators', label: '创作者' },
|
||||
@@ -9,6 +10,8 @@ const links = [
|
||||
];
|
||||
|
||||
export function OpsLayout() {
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<header>
|
||||
@@ -16,16 +19,25 @@ export function OpsLayout() {
|
||||
<p style={{ color: '#68716c' }}>内容审核、精选与数据概览</p>
|
||||
</header>
|
||||
<nav className="nav">
|
||||
{links.map((link) => (
|
||||
{opsLinks.map((link) => (
|
||||
<NavLink
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
end={link.to === '/'}
|
||||
end={link.end}
|
||||
className={({ isActive }) => (isActive ? 'active' : undefined)}
|
||||
>
|
||||
{link.label}
|
||||
</NavLink>
|
||||
))}
|
||||
{user?.role === 'admin' ? (
|
||||
<NavLink
|
||||
to="/admin"
|
||||
className={({ isActive }) => (isActive ? 'active' : undefined)}
|
||||
style={{ marginLeft: 'auto', opacity: 0.75 }}
|
||||
>
|
||||
⚙ 超管
|
||||
</NavLink>
|
||||
) : null}
|
||||
</nav>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useAuth } from '../lib/auth';
|
||||
import { mindSpaceLoginUrl } from '../lib/site';
|
||||
|
||||
export function RequireAdmin({ children }: { children: React.ReactNode }) {
|
||||
const { loading, user } = useAuth();
|
||||
|
||||
if (loading) return <p>检查权限…</p>;
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>需要登录</h2>
|
||||
<p>请先在 MindSpace 登录后再访问管理后台。</p>
|
||||
<a className="btn" href={mindSpaceLoginUrl()}>
|
||||
前往登录
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (user.role !== 'admin') {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>权限不足</h2>
|
||||
<p>超级管理后台需要 role = admin,当前账号 ({user.username}) 无此权限。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,45 +1,48 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchAuthStatus, fetchReviewQueue } from '../api/client';
|
||||
import { useAuth } from '../lib/auth';
|
||||
import { mindSpaceLoginUrl } from '../lib/site';
|
||||
import { fetchReviewQueue } from '../api/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function RequireOps({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState<'loading' | 'ok' | 'denied' | 'forbidden'>('loading');
|
||||
const { loading: authLoading, user } = useAuth();
|
||||
const [state, setState] = useState<'loading' | 'ok' | 'forbidden'>('loading');
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
if (!user) { setState('forbidden'); setMessage(null); return; }
|
||||
|
||||
// Admins bypass ops-role check.
|
||||
if (user.role === 'admin') { setState('ok'); return; }
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const auth = await fetchAuthStatus();
|
||||
if (!auth.authenticated) {
|
||||
setState('denied');
|
||||
return;
|
||||
}
|
||||
await fetchReviewQueue('status=pending_review&limit=1');
|
||||
setState('ok');
|
||||
} catch (err) {
|
||||
const text = err instanceof Error ? err.message : '无运营权限';
|
||||
if (text.includes('未授权') || text.includes('登录')) {
|
||||
setState('denied');
|
||||
} else {
|
||||
setMessage(text);
|
||||
setState('forbidden');
|
||||
}
|
||||
setMessage(err instanceof Error ? err.message : '无运营权限');
|
||||
setState('forbidden');
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
}, [authLoading, user]);
|
||||
|
||||
if (state === 'loading') return <p>检查登录态…</p>;
|
||||
if (state === 'denied') {
|
||||
if (authLoading || state === 'loading') return <p>检查登录态…</p>;
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>需要登录</h2>
|
||||
<p>请先在 MindSpace 登录,并确保账号已分配 ops_role(reviewer / editor / ops_admin)。</p>
|
||||
<p>请先在 MindSpace 登录(须与 Ops 使用同一域名,例如都用 127.0.0.1 或都用 *.localhost)。</p>
|
||||
<p style={{ color: '#68716c' }}>
|
||||
Ops 地址:<code>http://127.0.0.1:3002/ops/</code>
|
||||
</p>
|
||||
<a className="btn" href={mindSpaceLoginUrl()}>
|
||||
前往登录
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'forbidden') {
|
||||
return (
|
||||
<div className="card">
|
||||
@@ -51,5 +54,6 @@ export function RequireOps({ children }: { children: React.ReactNode }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user