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:
1 parent
f8b40353ef
commit
1d1ab384ef
16 files changed
+508
-5
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user