Merge branch 'ji' into master
按钮管理「登录参数」两套实现冲突解决——两分支各自独立给跳转按钮加了登录态追加能力, 二者正交、保留两套,互不影响: - master: login_params (JSON 列 login_params_json,从 userInfo 解析,类似轻应用登录参数) - ji: append_auth + auth_token_param + auth_name_param (token 登录时追加 token1+用户名) 迁移 revision 碰撞修复(两分支都用了 20260625_01):重编号为线性链 20260620_02 → 20260625_01 (login_params) → 20260625_02 (auth_params) → 20260625_03 (users_approved) 并同步 column_additions_mysql.sql 注释里的 migration 编号。 前后端 task_buttons(model/sql/memory/router/前端 type·api·admin 页)均保留两套字段; 两份 CLAUDE.md 文档合并为同时记录两种机制。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
58357547c9
44 files changed
+1207
-46
No files matched your search
@@ -33,6 +33,7 @@ import {
|
||||
} from '@/core/page-layout/sidebar-enabled';
|
||||
import { fetchYLogConfig } from '@/core/log/y-log-config';
|
||||
import { persistYLogToken } from '@/core/log/y-log-token';
|
||||
import { persistLoginCredentials } from '@/core/auth/login-credentials';
|
||||
import { OptType, writeYLog } from '@/core/log/y-log';
|
||||
|
||||
const DEFAULT_USERNAME = 'guest';
|
||||
@@ -197,6 +198,9 @@ export default function LoginPage() {
|
||||
// 持久化 token1(走 authStorage,嵌入态自动隔离)+ 预热上报配置 + 上报登录。
|
||||
if (authToken) {
|
||||
persistYLogToken(authToken);
|
||||
// 存下 token1 + 登录用户名(getTokenInfo 解析出的原始名,后端回传),供任务工作区
|
||||
// 跳转按钮的「追加登录态」拼到外链上(仅 token 登录才有这两个值)。
|
||||
persistLoginCredentials(authToken, auth);
|
||||
void fetchYLogConfig();
|
||||
writeYLog({
|
||||
optType: OptType.login,
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { Info, Search, ShieldCheck, UserCheck } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useAllUsers, useSetUserApproval } from "@/core/auth/hooks";
|
||||
import {
|
||||
useLoginWhitelistEnabled,
|
||||
useSetLoginWhitelistEnabled,
|
||||
} from "@/strategy-components/api/login-whitelist";
|
||||
|
||||
/**
|
||||
* 登录白名单管理(管理员)。
|
||||
*
|
||||
* 顶部一个「总开关」:关 = 所有人可登录;开 = 仅已放行用户(或 admin)可登录。
|
||||
* 下方列出全部注册用户,逐个放行 / 取消放行。用户仍可自助注册,注册后默认
|
||||
* 「待审批」,开关打开时被拦在登录外,直到管理员在此放行。
|
||||
*/
|
||||
export default function LoginWhitelistPage() {
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
|
||||
const { data: enabled, isLoading: loadingSwitch } = useLoginWhitelistEnabled();
|
||||
const setEnabled = useSetLoginWhitelistEnabled();
|
||||
|
||||
const { data: users = [], isLoading: loadingUsers } = useAllUsers(isAdmin);
|
||||
const setApproval = useSetUserApproval();
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
const visibleUsers = useMemo(
|
||||
() => (!q ? users : users.filter((u) => u.email.toLowerCase().includes(q) || u.id.toLowerCase().includes(q))),
|
||||
[users, q],
|
||||
);
|
||||
|
||||
const approvedCount = users.filter((u) => u.approved || u.system_role === "admin").length;
|
||||
|
||||
const handleToggleSwitch = async (next: boolean) => {
|
||||
try {
|
||||
await setEnabled.mutateAsync(next);
|
||||
toast.success(next ? "已启用登录白名单" : "已停用登录白名单(所有人可登录)");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "保存失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleApproval = async (userId: string, next: boolean) => {
|
||||
setPendingId(userId);
|
||||
try {
|
||||
await setApproval.mutateAsync({ userId, approved: next });
|
||||
toast.success(next ? "已放行该用户" : "已取消放行(下次登录被拦)");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "保存失败");
|
||||
} finally {
|
||||
setPendingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAdmin) return <Navigate to="/page/workspace/chats/new" replace />;
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto bg-slate-50/60 px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
{/* 顶部信息卡 + 总开关 */}
|
||||
<div className="relative mb-6 overflow-hidden rounded-3xl border border-slate-200/60 bg-white p-6 shadow-sm md:p-8">
|
||||
<div className="pointer-events-none absolute right-0 top-0 select-none p-8 opacity-[0.03]">
|
||||
<ShieldCheck className="size-44 text-slate-900" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="mb-4 flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
||||
<div>
|
||||
<span className="mb-3 inline-flex select-none items-center gap-1.5 rounded-full border border-red-100 bg-red-50 px-3 py-1 text-xs font-semibold text-red-600">
|
||||
<ShieldCheck className="size-3" />
|
||||
登录访问控制
|
||||
</span>
|
||||
<h1 className="flex items-center gap-3 text-2xl font-bold tracking-tight text-slate-800 md:text-3xl">
|
||||
白名单管理
|
||||
<span className="rounded-lg border border-slate-200/60 bg-slate-100 px-2.5 py-0.5 font-mono text-xs font-medium text-slate-500">
|
||||
{users.length} 用户 / {approvedCount} 可登录
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 rounded-2xl border border-slate-200/60 bg-slate-50/70 px-4 py-3">
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-semibold text-slate-700">登录白名单</div>
|
||||
<div className="text-xs text-slate-500">{enabled ? "已启用:仅已放行用户可登录" : "已停用:所有人可登录"}</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={Boolean(enabled)}
|
||||
disabled={loadingSwitch || setEnabled.isPending}
|
||||
onCheckedChange={handleToggleSwitch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="flex max-w-2xl items-start gap-2.5 rounded-2xl border border-slate-200/50 bg-slate-50/70 p-3.5 text-sm leading-relaxed text-slate-600">
|
||||
<Info className="mt-0.5 size-4 shrink-0 text-slate-400" />
|
||||
<span>
|
||||
用户仍可<strong>自助注册</strong>,注册后默认<strong> 待审批</strong>。开关<strong>关闭</strong>时所有人可正常登录使用;
|
||||
<strong> 开启</strong>后仅<strong>已放行</strong>的用户可登录,其余被拦在登录外,请在下方逐个放行。
|
||||
<strong> 管理员(admin)始终可登录</strong>,不受开关与放行状态影响。
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<div className="mb-6 rounded-2xl border border-slate-200/60 bg-white p-4 shadow-sm md:p-5">
|
||||
<div className="relative w-full md:w-80">
|
||||
<Search className="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索邮箱 / 用户ID…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50/50 py-2 pl-10 pr-4 text-sm transition-all hover:border-slate-300 focus:border-red-500 focus:outline-none focus:ring-4 focus:ring-red-500/5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户列表 */}
|
||||
<div className="overflow-hidden rounded-2xl border border-slate-200/60 bg-white shadow-sm">
|
||||
<div className="grid grid-cols-[1fr_auto_auto] items-center gap-3 border-b border-slate-200/60 bg-slate-50/70 px-4 py-2.5 text-xs font-semibold text-slate-500">
|
||||
<span>用户</span>
|
||||
<span className="px-1 text-center">问答</span>
|
||||
<span className="px-1 text-center">放行</span>
|
||||
</div>
|
||||
|
||||
{loadingUsers ? (
|
||||
<div className="px-4 py-12 text-center text-sm text-slate-400">加载中…</div>
|
||||
) : visibleUsers.length === 0 ? (
|
||||
<div className="px-4 py-12 text-center text-sm text-slate-400">
|
||||
{users.length === 0 ? "暂无注册用户" : "没有匹配的用户"}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{visibleUsers.map((user) => {
|
||||
const isUserAdmin = user.system_role === "admin";
|
||||
const allowed = user.approved || isUserAdmin;
|
||||
return (
|
||||
<li
|
||||
key={user.id}
|
||||
className="grid grid-cols-[1fr_auto_auto] items-center gap-3 px-4 py-3 transition-colors hover:bg-slate-50/60"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-medium text-slate-800">{user.email}</span>
|
||||
{isUserAdmin ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 px-1.5 py-0.5 text-[11px] font-semibold text-amber-600">
|
||||
<UserCheck className="size-3" />
|
||||
管理员
|
||||
</span>
|
||||
) : allowed ? (
|
||||
<span className="rounded-md border border-emerald-200 bg-emerald-50 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-600">
|
||||
已放行
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-md border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[11px] font-semibold text-slate-500">
|
||||
待审批
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate font-mono text-xs text-slate-400">{user.id}</div>
|
||||
</div>
|
||||
<div className="px-1 text-center text-sm tabular-nums text-slate-500">{user.qa_count}</div>
|
||||
<div className="px-1">
|
||||
<Switch
|
||||
checked={allowed}
|
||||
disabled={isUserAdmin || (setApproval.isPending && pendingId === user.id)}
|
||||
onCheckedChange={(v) => handleToggleApproval(user.id, v)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import LightAppIframeView from "./LightAppIframeView";
|
||||
import LightAppListPage from "./LightAppListPage";
|
||||
import LightAppManagementPage from "./LightAppManagementPage";
|
||||
import MenuManagementPage from "./MenuManagementPage";
|
||||
import LoginWhitelistPage from "./LoginWhitelistPage";
|
||||
import SensitiveWordsPage from "./SensitiveWordsPage";
|
||||
import SkillAddressesPage from "./SkillAddressesPage";
|
||||
import TaskButtonsPage from "./TaskButtonsPage";
|
||||
@@ -395,6 +396,16 @@ export default function StrategyRoutes() {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/login-whitelist"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<LoginWhitelistPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/roundtable-diagnostics"
|
||||
element={
|
||||
|
||||
@@ -108,6 +108,9 @@ export default function TaskButtonsPage() {
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
appendAuth: false,
|
||||
authTokenParam: "token",
|
||||
authNameParam: "username",
|
||||
enabled: true,
|
||||
sortOrder: maxSort + 1,
|
||||
},
|
||||
@@ -337,6 +340,37 @@ export default function TaskButtonsPage() {
|
||||
<SelectItem value="self">当前窗口</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex shrink-0 items-center gap-1.5 text-xs"
|
||||
title="跳转时在链接后追加 token + 用户名(仅 token 登录时生效)"
|
||||
>
|
||||
<Switch
|
||||
checked={r.appendAuth}
|
||||
onCheckedChange={(v) => update(r.id, { appendAuth: v })}
|
||||
/>
|
||||
追加登录态
|
||||
</label>
|
||||
{r.appendAuth && (
|
||||
<div className="flex shrink-0 items-center gap-1 text-xs">
|
||||
<span className="text-muted-foreground">&</span>
|
||||
<Input
|
||||
value={r.authTokenParam}
|
||||
onChange={(e) => update(r.id, { authTokenParam: e.target.value })}
|
||||
placeholder="token"
|
||||
className="h-8 w-24"
|
||||
title="token 的查询参数名(默认 token)"
|
||||
/>
|
||||
<span className="text-muted-foreground">=token1 &</span>
|
||||
<Input
|
||||
value={r.authNameParam}
|
||||
onChange={(e) => update(r.id, { authNameParam: e.target.value })}
|
||||
placeholder="username"
|
||||
className="h-8 w-28"
|
||||
title="用户名的查询参数名(默认 username)"
|
||||
/>
|
||||
<span className="text-muted-foreground">=用户名</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Select value={r.target} onValueChange={(v) => update(r.id, { target: v })}>
|
||||
|
||||
Reference in new issue
Block a user