白名单userid
This commit is contained in:
parent
9e72ff52c7
commit
7d08e7cfca
@ -3871,6 +3871,43 @@
|
||||
},
|
||||
"put": {
|
||||
"operationId": "replaceRegionBlocks",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["keywords"],
|
||||
"properties": {
|
||||
"keywords": {
|
||||
"type": "array",
|
||||
"maxItems": 200,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
},
|
||||
"whitelist_ips": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 64
|
||||
}
|
||||
},
|
||||
"whitelist_user_ids": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[1-9][0-9]*$",
|
||||
"maxLength": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
|
||||
@ -19,9 +19,18 @@ export interface IPWhitelistDto {
|
||||
updatedAtMs: number;
|
||||
}
|
||||
|
||||
export interface UserWhitelistDto {
|
||||
whitelistId: number;
|
||||
userId: string;
|
||||
enabled: boolean;
|
||||
createdAtMs: number;
|
||||
updatedAtMs: number;
|
||||
}
|
||||
|
||||
export interface RegionBlockPayload {
|
||||
keywords: string[];
|
||||
whitelist_ips?: string[];
|
||||
whitelist_user_ids?: string[];
|
||||
}
|
||||
|
||||
export interface AddRegionBlockWhitelistIPResult {
|
||||
@ -32,10 +41,13 @@ export interface AddRegionBlockWhitelistIPResult {
|
||||
export interface RegionBlockConfigDto extends ApiList<RegionBlockDto> {
|
||||
whitelistItems: IPWhitelistDto[];
|
||||
whitelistTotal: number;
|
||||
whitelistUserItems: UserWhitelistDto[];
|
||||
whitelistUserTotal: number;
|
||||
}
|
||||
|
||||
type RawRegionBlock = RegionBlockDto & Record<string, unknown>;
|
||||
type RawIPWhitelist = IPWhitelistDto & Record<string, unknown>;
|
||||
type RawUserWhitelist = UserWhitelistDto & Record<string, unknown>;
|
||||
type RawRegionBlockConfig = ApiList<RawRegionBlock> & Record<string, unknown>;
|
||||
|
||||
export function listRegionBlocks(): Promise<RegionBlockConfigDto> {
|
||||
@ -76,11 +88,14 @@ export async function addRegionBlockWhitelistIP(ipAddress: string): Promise<AddR
|
||||
|
||||
function normalizeRegionBlockConfig(data: RawRegionBlockConfig): RegionBlockConfigDto {
|
||||
const rawWhitelist = (data.whitelistItems ?? data.whitelist_items ?? []) as RawIPWhitelist[];
|
||||
const rawUserWhitelist = (data.whitelistUserItems ?? data.whitelist_user_items ?? []) as RawUserWhitelist[];
|
||||
return {
|
||||
items: (data.items || []).map(normalizeRegionBlock),
|
||||
total: Number(data.total || 0),
|
||||
whitelistItems: rawWhitelist.map(normalizeIPWhitelist),
|
||||
whitelistTotal: numberValue(data.whitelistTotal ?? data.whitelist_total),
|
||||
whitelistUserItems: rawUserWhitelist.map(normalizeUserWhitelist),
|
||||
whitelistUserTotal: numberValue(data.whitelistUserTotal ?? data.whitelist_user_total),
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,6 +120,16 @@ function normalizeIPWhitelist(item: RawIPWhitelist): IPWhitelistDto {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUserWhitelist(item: RawUserWhitelist): UserWhitelistDto {
|
||||
return {
|
||||
whitelistId: numberValue(item.whitelistId ?? item.whitelist_id),
|
||||
userId: stringValue(item.userId ?? item.user_id),
|
||||
enabled: Boolean(item.enabled),
|
||||
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
|
||||
updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms),
|
||||
};
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return value === undefined || value === null ? "" : String(value);
|
||||
}
|
||||
|
||||
@ -4,9 +4,14 @@ import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { listRegionBlocks, replaceRegionBlocks } from "@/features/region-blocks/api";
|
||||
import { useRegionBlockAbilities } from "@/features/region-blocks/permissions.js";
|
||||
import { regionBlockIPSchema, regionBlockKeywordSchema, regionBlockReplaceSchema } from "@/features/region-blocks/schema";
|
||||
import {
|
||||
regionBlockIPSchema,
|
||||
regionBlockKeywordSchema,
|
||||
regionBlockReplaceSchema,
|
||||
regionBlockUserIDSchema,
|
||||
} from "@/features/region-blocks/schema";
|
||||
|
||||
const emptyData = { items: [], total: 0, whitelistItems: [], whitelistTotal: 0 };
|
||||
const emptyData = { items: [], total: 0, whitelistItems: [], whitelistTotal: 0, whitelistUserItems: [], whitelistUserTotal: 0 };
|
||||
const countryCodePattern = /^[A-Za-z]{2,3}$/;
|
||||
|
||||
export function useRegionBlockPage() {
|
||||
@ -14,8 +19,10 @@ export function useRegionBlockPage() {
|
||||
const { showToast } = useToast();
|
||||
const [keywordInput, setKeywordInput] = useState("");
|
||||
const [ipInput, setIPInput] = useState("");
|
||||
const [userIDInput, setUserIDInput] = useState("");
|
||||
const [draftKeywords, setDraftKeywords] = useState([]);
|
||||
const [draftWhitelistIPs, setDraftWhitelistIPs] = useState([]);
|
||||
const [draftWhitelistUserIDs, setDraftWhitelistUserIDs] = useState([]);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [whitelistDialogOpen, setWhitelistDialogOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@ -34,8 +41,13 @@ export function useRegionBlockPage() {
|
||||
() => (data.whitelistItems || []).map((item) => item.ipAddress).filter(Boolean),
|
||||
[data.whitelistItems],
|
||||
);
|
||||
const serverWhitelistUserIDs = useMemo(
|
||||
() => (data.whitelistUserItems || []).map((item) => item.userId).filter(Boolean),
|
||||
[data.whitelistUserItems],
|
||||
);
|
||||
const serverKeywordKey = serverKeywords.join("\n");
|
||||
const serverWhitelistKey = serverWhitelistIPs.join("\n");
|
||||
const serverWhitelistUserKey = serverWhitelistUserIDs.join("\n");
|
||||
const serverBlockByKeyword = useMemo(() => {
|
||||
const blocks = new Map();
|
||||
(data.items || []).forEach((item) => blocks.set(normalizeKey(item.keyword), item));
|
||||
@ -46,13 +58,19 @@ export function useRegionBlockPage() {
|
||||
(data.whitelistItems || []).forEach((item) => items.set(normalizeKey(item.ipAddress), item));
|
||||
return items;
|
||||
}, [data.whitelistItems]);
|
||||
const serverWhitelistByUserID = useMemo(() => {
|
||||
const items = new Map();
|
||||
(data.whitelistUserItems || []).forEach((item) => items.set(normalizeKey(item.userId), item));
|
||||
return items;
|
||||
}, [data.whitelistUserItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty) {
|
||||
setDraftKeywords(serverKeywords);
|
||||
setDraftWhitelistIPs(serverWhitelistIPs);
|
||||
setDraftWhitelistUserIDs(serverWhitelistUserIDs);
|
||||
}
|
||||
}, [dirty, serverKeywordKey, serverKeywords, serverWhitelistIPs, serverWhitelistKey]);
|
||||
}, [dirty, serverKeywordKey, serverKeywords, serverWhitelistIPs, serverWhitelistKey, serverWhitelistUserIDs, serverWhitelistUserKey]);
|
||||
|
||||
const draftItems = useMemo(
|
||||
() =>
|
||||
@ -87,6 +105,22 @@ export function useRegionBlockPage() {
|
||||
}),
|
||||
[draftWhitelistIPs, serverWhitelistByIP],
|
||||
);
|
||||
const draftWhitelistUserItems = useMemo(
|
||||
() =>
|
||||
draftWhitelistUserIDs.map((userID, index) => {
|
||||
const saved = serverWhitelistByUserID.get(normalizeKey(userID));
|
||||
return (
|
||||
saved || {
|
||||
whitelistId: `draft-user-${index}-${userID}`,
|
||||
userId: userID,
|
||||
createdAtMs: 0,
|
||||
enabled: true,
|
||||
updatedAtMs: 0,
|
||||
}
|
||||
);
|
||||
}),
|
||||
[draftWhitelistUserIDs, serverWhitelistByUserID],
|
||||
);
|
||||
|
||||
const addKeyword = () => {
|
||||
if (!abilities.canUpdate) {
|
||||
@ -134,6 +168,29 @@ export function useRegionBlockPage() {
|
||||
addIP();
|
||||
};
|
||||
|
||||
const addUserID = () => {
|
||||
if (!abilities.canUpdate) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const userID = parseForm(regionBlockUserIDSchema, userIDInput);
|
||||
if (draftWhitelistUserIDs.some((item) => normalizeKey(item) === normalizeKey(userID))) {
|
||||
showToast("白名单用户已存在", "warning");
|
||||
return;
|
||||
}
|
||||
setDraftWhitelistUserIDs([...draftWhitelistUserIDs, userID]);
|
||||
setUserIDInput("");
|
||||
setDirty(true);
|
||||
} catch (err) {
|
||||
showValidationError(err, showToast, "用户 ID 参数不正确");
|
||||
}
|
||||
};
|
||||
|
||||
const submitUserIDInput = (event) => {
|
||||
event.preventDefault();
|
||||
addUserID();
|
||||
};
|
||||
|
||||
const removeKeyword = (keyword) => {
|
||||
if (!abilities.canUpdate) {
|
||||
return;
|
||||
@ -150,11 +207,21 @@ export function useRegionBlockPage() {
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const removeUserID = (userID) => {
|
||||
if (!abilities.canUpdate) {
|
||||
return;
|
||||
}
|
||||
setDraftWhitelistUserIDs(draftWhitelistUserIDs.filter((item) => normalizeKey(item) !== normalizeKey(userID)));
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const resetDraft = () => {
|
||||
setDraftKeywords(serverKeywords);
|
||||
setDraftWhitelistIPs(serverWhitelistIPs);
|
||||
setDraftWhitelistUserIDs(serverWhitelistUserIDs);
|
||||
setKeywordInput("");
|
||||
setIPInput("");
|
||||
setUserIDInput("");
|
||||
setDirty(false);
|
||||
};
|
||||
|
||||
@ -164,10 +231,15 @@ export function useRegionBlockPage() {
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = parseForm(regionBlockReplaceSchema, { keywords: draftKeywords, whitelist_ips: draftWhitelistIPs });
|
||||
const payload = parseForm(regionBlockReplaceSchema, {
|
||||
keywords: draftKeywords,
|
||||
whitelist_ips: draftWhitelistIPs,
|
||||
whitelist_user_ids: draftWhitelistUserIDs,
|
||||
});
|
||||
const saved = await replaceRegionBlocks(payload);
|
||||
setDraftKeywords((saved.items || []).map((item) => item.keyword).filter(Boolean));
|
||||
setDraftWhitelistIPs((saved.whitelistItems || []).map((item) => item.ipAddress).filter(Boolean));
|
||||
setDraftWhitelistUserIDs((saved.whitelistUserItems || []).map((item) => item.userId).filter(Boolean));
|
||||
setDirty(false);
|
||||
showToast("地区屏蔽配置已保存", "success");
|
||||
await reload();
|
||||
@ -182,9 +254,11 @@ export function useRegionBlockPage() {
|
||||
abilities,
|
||||
addIP,
|
||||
addKeyword,
|
||||
addUserID,
|
||||
dirty,
|
||||
draftItems,
|
||||
draftWhitelistItems,
|
||||
draftWhitelistUserItems,
|
||||
error,
|
||||
ipInput,
|
||||
keywordInput,
|
||||
@ -193,13 +267,17 @@ export function useRegionBlockPage() {
|
||||
reload,
|
||||
removeIP,
|
||||
removeKeyword,
|
||||
removeUserID,
|
||||
resetDraft,
|
||||
save,
|
||||
saving,
|
||||
setIPInput,
|
||||
setKeywordInput,
|
||||
setUserIDInput,
|
||||
submitIPInput,
|
||||
submitInput,
|
||||
submitUserIDInput,
|
||||
userIDInput,
|
||||
whitelistDialogOpen,
|
||||
closeWhitelistDialog: () => setWhitelistDialogOpen(false),
|
||||
};
|
||||
|
||||
@ -23,6 +23,7 @@ export function RegionBlockPage() {
|
||||
const page = useRegionBlockPage();
|
||||
const columns = buildColumns(page);
|
||||
const whitelistColumns = buildWhitelistColumns(page);
|
||||
const userWhitelistColumns = buildUserWhitelistColumns(page);
|
||||
|
||||
return (
|
||||
<AdminListPage>
|
||||
@ -100,6 +101,25 @@ export function RegionBlockPage() {
|
||||
>
|
||||
白名单IP列表
|
||||
</Button>
|
||||
<form className={styles.addForm} onSubmit={page.submitUserIDInput}>
|
||||
<TextField
|
||||
className={styles.userInput}
|
||||
disabled={!page.abilities.canUpdate || page.saving}
|
||||
label="用户ID白名单"
|
||||
placeholder="900025"
|
||||
size="small"
|
||||
value={page.userIDInput}
|
||||
onChange={(event) => page.setUserIDInput(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={!page.abilities.canUpdate || page.saving}
|
||||
startIcon={<AddOutlined fontSize="small" />}
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
添加用户
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@ -123,6 +143,15 @@ export function RegionBlockPage() {
|
||||
rowKey={(item) => item.whitelistId || item.ipAddress}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.tableGroup}>
|
||||
<h2 className={styles.tableTitle}>用户ID白名单</h2>
|
||||
<DataTable
|
||||
columns={userWhitelistColumns}
|
||||
items={page.draftWhitelistUserItems}
|
||||
minWidth="720px"
|
||||
rowKey={(item) => item.whitelistId || item.userId}
|
||||
/>
|
||||
</div>
|
||||
</AdminListBody>
|
||||
</DataState>
|
||||
<WhitelistDialog page={page} />
|
||||
@ -208,6 +237,42 @@ function buildWhitelistColumns(page) {
|
||||
];
|
||||
}
|
||||
|
||||
function buildUserWhitelistColumns(page) {
|
||||
return [
|
||||
{
|
||||
key: "userId",
|
||||
label: "用户ID",
|
||||
width: "minmax(220px, 1fr)",
|
||||
render: (item) => <KeywordCell item={{ keyword: item.userId, enabled: item.enabled }} />,
|
||||
},
|
||||
{
|
||||
key: "updatedAtMs",
|
||||
label: "更新时间",
|
||||
width: "minmax(180px, 0.8fr)",
|
||||
render: (item) => (item.updatedAtMs ? formatMillis(item.updatedAtMs) : "-"),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "操作",
|
||||
width: "minmax(96px, 0.4fr)",
|
||||
render: (item) => (
|
||||
<Tooltip arrow title="移除">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={!page.abilities.canUpdate || page.saving}
|
||||
label="移除"
|
||||
tone="danger"
|
||||
onClick={() => page.removeUserID(item.userId)}
|
||||
>
|
||||
<DeleteOutlineOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildWhitelistDialogColumns() {
|
||||
return [
|
||||
{
|
||||
|
||||
@ -20,6 +20,10 @@
|
||||
flex: 0 1 220px;
|
||||
}
|
||||
|
||||
.userInput {
|
||||
flex: 0 1 220px;
|
||||
}
|
||||
|
||||
.tableGroup {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
@ -112,7 +116,8 @@
|
||||
}
|
||||
|
||||
.keywordInput,
|
||||
.ipInput {
|
||||
.ipInput,
|
||||
.userInput {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -13,9 +13,17 @@ export const regionBlockIPSchema = z
|
||||
.max(64, "IP 不能超过 64 个字符")
|
||||
.refine((value) => isIPv4(value) || isIPv6(value), "请输入正确的 IP 地址");
|
||||
|
||||
export const regionBlockUserIDSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "请输入用户 ID")
|
||||
.max(20, "用户 ID 不能超过 20 位")
|
||||
.regex(/^[1-9]\d*$/, "请输入正确的用户 ID");
|
||||
|
||||
export const regionBlockReplaceSchema = z.object({
|
||||
keywords: z.array(regionBlockKeywordSchema).max(200, "最多配置 200 个屏蔽词"),
|
||||
whitelist_ips: z.array(regionBlockIPSchema).max(500, "最多配置 500 个白名单 IP"),
|
||||
whitelist_user_ids: z.array(regionBlockUserIDSchema).max(500, "最多配置 500 个白名单用户"),
|
||||
});
|
||||
|
||||
export type RegionBlockReplaceForm = z.infer<typeof regionBlockReplaceSchema>;
|
||||
|
||||
10
src/shared/api/generated/schema.d.ts
vendored
10
src/shared/api/generated/schema.d.ts
vendored
@ -7051,7 +7051,15 @@ export interface operations {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": {
|
||||
keywords: string[];
|
||||
whitelist_ips?: string[];
|
||||
whitelist_user_ids?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user