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

@@ -0,0 +1,68 @@
/**
* 登录白名单「总开关」API 客户端 + React Query hooks。
*
* 后端:`GET|PUT /api/system-settings/login-whitelist`(admin only)。
* 开关本身存系统设置 JSON,即时生效、免重启;逐用户的放行标记走
* `core/auth`(`UserListItem.approved` + `setUserApproval`)。
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
import { OptType, writeYLog } from "@/core/log/y-log";
const BASE = () => `${getBackendBaseURL()}/api/system-settings/login-whitelist`;
async function readJson<T>(res: Response, ctx: string): Promise<T> {
if (!res.ok) {
const data = await res.json().catch(() => null as unknown);
const detail =
data && typeof data === "object" && "detail" in data
? String((data as { detail: unknown }).detail)
: `${ctx} failed (${res.status})`;
throw new Error(detail);
}
return (await res.json()) as T;
}
export async function getLoginWhitelistEnabled(): Promise<boolean> {
const res = await apiFetch(BASE(), { method: "GET" });
const data = await readJson<{ enabled: boolean }>(res, "load login whitelist");
return Boolean(data.enabled);
}
export async function setLoginWhitelistEnabled(enabled: boolean): Promise<boolean> {
const res = await apiFetch(BASE(), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled }),
});
const data = await readJson<{ enabled: boolean }>(res, "save login whitelist");
writeYLog({
optType: enabled ? OptType.enabled : OptType.disabled,
optDetail: `CM助手-全量-白名单管理-${enabled ? "启用" : "停用"}登录白名单`,
url: "/api/system-settings/login-whitelist",
reqParam: { enabled },
});
return Boolean(data.enabled);
}
export const LOGIN_WHITELIST_KEY = "login-whitelist-enabled";
export function useLoginWhitelistEnabled() {
return useQuery({
queryKey: [LOGIN_WHITELIST_KEY],
queryFn: getLoginWhitelistEnabled,
refetchOnWindowFocus: false,
staleTime: 60_000,
});
}
export function useSetLoginWhitelistEnabled() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: setLoginWhitelistEnabled,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [LOGIN_WHITELIST_KEY] }),
});
}