feat(auth): 登录白名单(注册后管理员审批放行 + 全局开关)

用户仍可自助注册,普通账号默认 approved=False(待审批),admin 建号即 True;
存量用户迁移回填 True 避免锁死。系统设置 login_whitelist.enabled 总开关:关=
所有人可登录,开=仅已放行用户(或 admin)可登录。三登录端点建号后、发 token 前
经 enforce_user_approved 拦截,未放行返回 403 PENDING_APPROVAL。管理员在「白名单
管理」页(设置下拉入口)切总开关 + 逐用户放行。取消放行只拦下次登录。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-25 16:09:38 +08:00
1 parent f8b40353ef
commit 1d1ab384ef
16 files changed
+508 -5

No files matched your search

+16
View File
@@ -11,6 +11,8 @@ export interface UserListItem {
agent_count: number;
/** 问答次数:用户提问的次数(一次提问算一次)。 */
qa_count: number;
/** 登录白名单放行标记:开关开启后仅 approved 用户(或 admin)可登录。 */
approved: boolean;
}
export async function listAllUsers(): Promise<UserListItem[]> {
@@ -19,6 +21,20 @@ export async function listAllUsers(): Promise<UserListItem[]> {
return res.json() as Promise<UserListItem[]>;
}
/** 放行 / 取消放行某个注册用户(登录白名单)。Admin only。 */
export async function setUserApproval(userId: string, approved: boolean): Promise<UserListItem> {
const res = await apiFetch(`${getBackendBaseURL()}/api/v1/auth/users/${encodeURIComponent(userId)}/approval`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ approved }),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
throw new Error(extractErrorMessage(data, "保存失败"));
}
return res.json() as Promise<UserListItem>;
}
// Normalize a FastAPI error body into a human-readable string. `detail` may be
// a plain string, an object like AuthErrorResponse ({ code, message }), or an
// array of validation errors — anything but a string would otherwise render as
+21 -2
View File
@@ -1,6 +1,8 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { listAllUsers } from "./api";
import { OptType, writeYLog } from "@/core/log/y-log";
import { listAllUsers, setUserApproval } from "./api";
export function useAllUsers(enabled: boolean) {
return useQuery({
@@ -9,3 +11,20 @@ export function useAllUsers(enabled: boolean) {
enabled,
});
}
/** 放行 / 取消放行某个用户(登录白名单),成功后刷新用户列表缓存。 */
export function useSetUserApproval() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, approved }: { userId: string; approved: boolean }) => setUserApproval(userId, approved),
onSuccess: (_data, vars) => {
writeYLog({
optType: vars.approved ? OptType.enabled : OptType.disabled,
optDetail: `CM助手-全量-白名单管理-${vars.approved ? "放行" : "取消放行"}用户`,
url: `/api/v1/auth/users/${vars.userId}/approval`,
reqParam: vars,
});
void queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
},
});
}