ip白名单
This commit is contained in:
parent
8c23d7e6a7
commit
17251552d9
@ -11,34 +11,54 @@ export interface RegionBlockDto {
|
|||||||
updatedAtMs: number;
|
updatedAtMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPWhitelistDto {
|
||||||
|
whitelistId: number;
|
||||||
|
ipAddress: string;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAtMs: number;
|
||||||
|
updatedAtMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RegionBlockPayload {
|
export interface RegionBlockPayload {
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
|
whitelist_ips?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegionBlockConfigDto extends ApiList<RegionBlockDto> {
|
||||||
|
whitelistItems: IPWhitelistDto[];
|
||||||
|
whitelistTotal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawRegionBlock = RegionBlockDto & Record<string, unknown>;
|
type RawRegionBlock = RegionBlockDto & Record<string, unknown>;
|
||||||
|
type RawIPWhitelist = IPWhitelistDto & Record<string, unknown>;
|
||||||
|
type RawRegionBlockConfig = ApiList<RawRegionBlock> & Record<string, unknown>;
|
||||||
|
|
||||||
export function listRegionBlocks(): Promise<ApiList<RegionBlockDto>> {
|
export function listRegionBlocks(): Promise<RegionBlockConfigDto> {
|
||||||
const endpoint = API_ENDPOINTS.listRegionBlocks;
|
const endpoint = API_ENDPOINTS.listRegionBlocks;
|
||||||
return apiRequest<ApiList<RawRegionBlock>>(apiEndpointPath(API_OPERATIONS.listRegionBlocks), {
|
return apiRequest<RawRegionBlockConfig>(apiEndpointPath(API_OPERATIONS.listRegionBlocks), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
}).then((data) => ({
|
}).then(normalizeRegionBlockConfig);
|
||||||
items: (data.items || []).map(normalizeRegionBlock),
|
|
||||||
total: Number(data.total || 0),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceRegionBlocks(payload: RegionBlockPayload): Promise<ApiList<RegionBlockDto>> {
|
export function replaceRegionBlocks(payload: RegionBlockPayload): Promise<RegionBlockConfigDto> {
|
||||||
const endpoint = API_ENDPOINTS.replaceRegionBlocks;
|
const endpoint = API_ENDPOINTS.replaceRegionBlocks;
|
||||||
return apiRequest<ApiList<RawRegionBlock>, RegionBlockPayload>(
|
return apiRequest<RawRegionBlockConfig, RegionBlockPayload>(
|
||||||
apiEndpointPath(API_OPERATIONS.replaceRegionBlocks),
|
apiEndpointPath(API_OPERATIONS.replaceRegionBlocks),
|
||||||
{
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
},
|
},
|
||||||
).then((data) => ({
|
).then(normalizeRegionBlockConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRegionBlockConfig(data: RawRegionBlockConfig): RegionBlockConfigDto {
|
||||||
|
const rawWhitelist = (data.whitelistItems ?? data.whitelist_items ?? []) as RawIPWhitelist[];
|
||||||
|
return {
|
||||||
items: (data.items || []).map(normalizeRegionBlock),
|
items: (data.items || []).map(normalizeRegionBlock),
|
||||||
total: Number(data.total || 0),
|
total: Number(data.total || 0),
|
||||||
}));
|
whitelistItems: rawWhitelist.map(normalizeIPWhitelist),
|
||||||
|
whitelistTotal: numberValue(data.whitelistTotal ?? data.whitelist_total),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRegionBlock(item: RawRegionBlock): RegionBlockDto {
|
function normalizeRegionBlock(item: RawRegionBlock): RegionBlockDto {
|
||||||
@ -52,6 +72,16 @@ function normalizeRegionBlock(item: RawRegionBlock): RegionBlockDto {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeIPWhitelist(item: RawIPWhitelist): IPWhitelistDto {
|
||||||
|
return {
|
||||||
|
whitelistId: numberValue(item.whitelistId ?? item.whitelist_id),
|
||||||
|
ipAddress: stringValue(item.ipAddress ?? item.ip_address),
|
||||||
|
enabled: Boolean(item.enabled),
|
||||||
|
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
|
||||||
|
updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function stringValue(value: unknown) {
|
function stringValue(value: unknown) {
|
||||||
return value === undefined || value === null ? "" : String(value);
|
return value === undefined || value === null ? "" : String(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,16 +4,18 @@ import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
|||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import { listRegionBlocks, replaceRegionBlocks } from "@/features/region-blocks/api";
|
import { listRegionBlocks, replaceRegionBlocks } from "@/features/region-blocks/api";
|
||||||
import { useRegionBlockAbilities } from "@/features/region-blocks/permissions.js";
|
import { useRegionBlockAbilities } from "@/features/region-blocks/permissions.js";
|
||||||
import { regionBlockKeywordSchema, regionBlockReplaceSchema } from "@/features/region-blocks/schema";
|
import { regionBlockIPSchema, regionBlockKeywordSchema, regionBlockReplaceSchema } from "@/features/region-blocks/schema";
|
||||||
|
|
||||||
const emptyData = { items: [], total: 0 };
|
const emptyData = { items: [], total: 0, whitelistItems: [], whitelistTotal: 0 };
|
||||||
const countryCodePattern = /^[A-Za-z]{2,3}$/;
|
const countryCodePattern = /^[A-Za-z]{2,3}$/;
|
||||||
|
|
||||||
export function useRegionBlockPage() {
|
export function useRegionBlockPage() {
|
||||||
const abilities = useRegionBlockAbilities();
|
const abilities = useRegionBlockAbilities();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [keywordInput, setKeywordInput] = useState("");
|
const [keywordInput, setKeywordInput] = useState("");
|
||||||
|
const [ipInput, setIPInput] = useState("");
|
||||||
const [draftKeywords, setDraftKeywords] = useState([]);
|
const [draftKeywords, setDraftKeywords] = useState([]);
|
||||||
|
const [draftWhitelistIPs, setDraftWhitelistIPs] = useState([]);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const {
|
const {
|
||||||
@ -27,18 +29,29 @@ export function useRegionBlockPage() {
|
|||||||
queryKey: ["region-blocks"],
|
queryKey: ["region-blocks"],
|
||||||
});
|
});
|
||||||
const serverKeywords = useMemo(() => (data.items || []).map((item) => item.keyword).filter(Boolean), [data.items]);
|
const serverKeywords = useMemo(() => (data.items || []).map((item) => item.keyword).filter(Boolean), [data.items]);
|
||||||
|
const serverWhitelistIPs = useMemo(
|
||||||
|
() => (data.whitelistItems || []).map((item) => item.ipAddress).filter(Boolean),
|
||||||
|
[data.whitelistItems],
|
||||||
|
);
|
||||||
const serverKeywordKey = serverKeywords.join("\n");
|
const serverKeywordKey = serverKeywords.join("\n");
|
||||||
|
const serverWhitelistKey = serverWhitelistIPs.join("\n");
|
||||||
const serverBlockByKeyword = useMemo(() => {
|
const serverBlockByKeyword = useMemo(() => {
|
||||||
const blocks = new Map();
|
const blocks = new Map();
|
||||||
(data.items || []).forEach((item) => blocks.set(normalizeKey(item.keyword), item));
|
(data.items || []).forEach((item) => blocks.set(normalizeKey(item.keyword), item));
|
||||||
return blocks;
|
return blocks;
|
||||||
}, [data.items]);
|
}, [data.items]);
|
||||||
|
const serverWhitelistByIP = useMemo(() => {
|
||||||
|
const items = new Map();
|
||||||
|
(data.whitelistItems || []).forEach((item) => items.set(normalizeKey(item.ipAddress), item));
|
||||||
|
return items;
|
||||||
|
}, [data.whitelistItems]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dirty) {
|
if (!dirty) {
|
||||||
setDraftKeywords(serverKeywords);
|
setDraftKeywords(serverKeywords);
|
||||||
|
setDraftWhitelistIPs(serverWhitelistIPs);
|
||||||
}
|
}
|
||||||
}, [dirty, serverKeywordKey, serverKeywords]);
|
}, [dirty, serverKeywordKey, serverKeywords, serverWhitelistIPs, serverWhitelistKey]);
|
||||||
|
|
||||||
const draftItems = useMemo(
|
const draftItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -57,6 +70,22 @@ export function useRegionBlockPage() {
|
|||||||
}),
|
}),
|
||||||
[draftKeywords, serverBlockByKeyword],
|
[draftKeywords, serverBlockByKeyword],
|
||||||
);
|
);
|
||||||
|
const draftWhitelistItems = useMemo(
|
||||||
|
() =>
|
||||||
|
draftWhitelistIPs.map((ip, index) => {
|
||||||
|
const saved = serverWhitelistByIP.get(normalizeKey(ip));
|
||||||
|
return (
|
||||||
|
saved || {
|
||||||
|
whitelistId: `draft-${index}-${ip}`,
|
||||||
|
ipAddress: ip,
|
||||||
|
createdAtMs: 0,
|
||||||
|
enabled: true,
|
||||||
|
updatedAtMs: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[draftWhitelistIPs, serverWhitelistByIP],
|
||||||
|
);
|
||||||
|
|
||||||
const addKeyword = () => {
|
const addKeyword = () => {
|
||||||
if (!abilities.canUpdate) {
|
if (!abilities.canUpdate) {
|
||||||
@ -81,6 +110,29 @@ export function useRegionBlockPage() {
|
|||||||
addKeyword();
|
addKeyword();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addIP = () => {
|
||||||
|
if (!abilities.canUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const ip = parseForm(regionBlockIPSchema, ipInput);
|
||||||
|
if (draftWhitelistIPs.some((item) => normalizeKey(item) === normalizeKey(ip))) {
|
||||||
|
showToast("白名单 IP 已存在", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDraftWhitelistIPs([...draftWhitelistIPs, ip]);
|
||||||
|
setIPInput("");
|
||||||
|
setDirty(true);
|
||||||
|
} catch (err) {
|
||||||
|
showValidationError(err, showToast, "IP 参数不正确");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitIPInput = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
addIP();
|
||||||
|
};
|
||||||
|
|
||||||
const removeKeyword = (keyword) => {
|
const removeKeyword = (keyword) => {
|
||||||
if (!abilities.canUpdate) {
|
if (!abilities.canUpdate) {
|
||||||
return;
|
return;
|
||||||
@ -89,9 +141,19 @@ export function useRegionBlockPage() {
|
|||||||
setDirty(true);
|
setDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeIP = (ip) => {
|
||||||
|
if (!abilities.canUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDraftWhitelistIPs(draftWhitelistIPs.filter((item) => normalizeKey(item) !== normalizeKey(ip)));
|
||||||
|
setDirty(true);
|
||||||
|
};
|
||||||
|
|
||||||
const resetDraft = () => {
|
const resetDraft = () => {
|
||||||
setDraftKeywords(serverKeywords);
|
setDraftKeywords(serverKeywords);
|
||||||
|
setDraftWhitelistIPs(serverWhitelistIPs);
|
||||||
setKeywordInput("");
|
setKeywordInput("");
|
||||||
|
setIPInput("");
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,9 +163,10 @@ export function useRegionBlockPage() {
|
|||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload = parseForm(regionBlockReplaceSchema, { keywords: draftKeywords });
|
const payload = parseForm(regionBlockReplaceSchema, { keywords: draftKeywords, whitelist_ips: draftWhitelistIPs });
|
||||||
const saved = await replaceRegionBlocks(payload);
|
const saved = await replaceRegionBlocks(payload);
|
||||||
setDraftKeywords((saved.items || []).map((item) => item.keyword).filter(Boolean));
|
setDraftKeywords((saved.items || []).map((item) => item.keyword).filter(Boolean));
|
||||||
|
setDraftWhitelistIPs((saved.whitelistItems || []).map((item) => item.ipAddress).filter(Boolean));
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
showToast("地区屏蔽配置已保存", "success");
|
showToast("地区屏蔽配置已保存", "success");
|
||||||
await reload();
|
await reload();
|
||||||
@ -116,18 +179,24 @@ export function useRegionBlockPage() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
|
addIP,
|
||||||
addKeyword,
|
addKeyword,
|
||||||
dirty,
|
dirty,
|
||||||
draftItems,
|
draftItems,
|
||||||
|
draftWhitelistItems,
|
||||||
error,
|
error,
|
||||||
|
ipInput,
|
||||||
keywordInput,
|
keywordInput,
|
||||||
loading,
|
loading,
|
||||||
reload,
|
reload,
|
||||||
|
removeIP,
|
||||||
removeKeyword,
|
removeKeyword,
|
||||||
resetDraft,
|
resetDraft,
|
||||||
save,
|
save,
|
||||||
saving,
|
saving,
|
||||||
|
setIPInput,
|
||||||
setKeywordInput,
|
setKeywordInput,
|
||||||
|
submitIPInput,
|
||||||
submitInput,
|
submitInput,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import styles from "@/features/region-blocks/region-blocks.module.css";
|
|||||||
export function RegionBlockPage() {
|
export function RegionBlockPage() {
|
||||||
const page = useRegionBlockPage();
|
const page = useRegionBlockPage();
|
||||||
const columns = buildColumns(page);
|
const columns = buildColumns(page);
|
||||||
|
const whitelistColumns = buildWhitelistColumns(page);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
@ -48,35 +49,68 @@ export function RegionBlockPage() {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
filters={
|
filters={
|
||||||
<form className={styles.addForm} onSubmit={page.submitInput}>
|
<div className={styles.forms}>
|
||||||
<TextField
|
<form className={styles.addForm} onSubmit={page.submitInput}>
|
||||||
className={styles.keywordInput}
|
<TextField
|
||||||
disabled={!page.abilities.canUpdate || page.saving}
|
className={styles.keywordInput}
|
||||||
label="屏蔽词"
|
disabled={!page.abilities.canUpdate || page.saving}
|
||||||
placeholder="国家码或国家名称"
|
label="屏蔽词"
|
||||||
size="small"
|
placeholder="国家码或国家名称"
|
||||||
value={page.keywordInput}
|
size="small"
|
||||||
onChange={(event) => page.setKeywordInput(event.target.value)}
|
value={page.keywordInput}
|
||||||
/>
|
onChange={(event) => page.setKeywordInput(event.target.value)}
|
||||||
<Button
|
/>
|
||||||
disabled={!page.abilities.canUpdate || page.saving}
|
<Button
|
||||||
startIcon={<AddOutlined fontSize="small" />}
|
disabled={!page.abilities.canUpdate || page.saving}
|
||||||
type="submit"
|
startIcon={<AddOutlined fontSize="small" />}
|
||||||
variant="primary"
|
type="submit"
|
||||||
>
|
variant="primary"
|
||||||
添加
|
>
|
||||||
</Button>
|
添加屏蔽词
|
||||||
</form>
|
</Button>
|
||||||
|
</form>
|
||||||
|
<form className={styles.addForm} onSubmit={page.submitIPInput}>
|
||||||
|
<TextField
|
||||||
|
className={styles.ipInput}
|
||||||
|
disabled={!page.abilities.canUpdate || page.saving}
|
||||||
|
label="IP白名单"
|
||||||
|
placeholder="203.0.113.10"
|
||||||
|
size="small"
|
||||||
|
value={page.ipInput}
|
||||||
|
onChange={(event) => page.setIPInput(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={!page.abilities.canUpdate || page.saving}
|
||||||
|
startIcon={<AddOutlined fontSize="small" />}
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
添加IP
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<AdminListBody>
|
<AdminListBody>
|
||||||
<DataTable
|
<div className={styles.tableGroup}>
|
||||||
columns={columns}
|
<h2 className={styles.tableTitle}>地区屏蔽</h2>
|
||||||
items={page.draftItems}
|
<DataTable
|
||||||
minWidth="880px"
|
columns={columns}
|
||||||
rowKey={(item) => item.blockId || item.keyword}
|
items={page.draftItems}
|
||||||
/>
|
minWidth="880px"
|
||||||
|
rowKey={(item) => item.blockId || item.keyword}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.tableGroup}>
|
||||||
|
<h2 className={styles.tableTitle}>IP白名单</h2>
|
||||||
|
<DataTable
|
||||||
|
columns={whitelistColumns}
|
||||||
|
items={page.draftWhitelistItems}
|
||||||
|
minWidth="720px"
|
||||||
|
rowKey={(item) => item.whitelistId || item.ipAddress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</AdminListBody>
|
</AdminListBody>
|
||||||
</DataState>
|
</DataState>
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
@ -125,6 +159,42 @@ function buildColumns(page) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildWhitelistColumns(page) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "ipAddress",
|
||||||
|
label: "IP",
|
||||||
|
width: "minmax(220px, 1fr)",
|
||||||
|
render: (item) => <KeywordCell item={{ keyword: item.ipAddress, 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.removeIP(item.ipAddress)}
|
||||||
|
>
|
||||||
|
<DeleteOutlineOutlined fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function KeywordCell({ item }) {
|
function KeywordCell({ item }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.keywordCell}>
|
<div className={styles.keywordCell}>
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
.forms {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
.addForm {
|
.addForm {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -9,6 +16,27 @@
|
|||||||
flex: 0 1 320px;
|
flex: 0 1 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ipInput {
|
||||||
|
flex: 0 1 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableGroup {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableGroup + .tableGroup {
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableTitle {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--admin-font-size-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.keywordCell {
|
.keywordCell {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -29,12 +57,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
|
.forms {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.addForm {
|
.addForm {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.keywordInput {
|
.keywordInput,
|
||||||
|
.ipInput {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,34 @@ export const regionBlockKeywordSchema = z
|
|||||||
.min(1, "请输入屏蔽词")
|
.min(1, "请输入屏蔽词")
|
||||||
.max(128, "屏蔽词不能超过 128 个字符");
|
.max(128, "屏蔽词不能超过 128 个字符");
|
||||||
|
|
||||||
|
export const regionBlockIPSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "请输入 IP")
|
||||||
|
.max(64, "IP 不能超过 64 个字符")
|
||||||
|
.refine((value) => isIPv4(value) || isIPv6(value), "请输入正确的 IP 地址");
|
||||||
|
|
||||||
export const regionBlockReplaceSchema = z.object({
|
export const regionBlockReplaceSchema = z.object({
|
||||||
keywords: z.array(regionBlockKeywordSchema).max(200, "最多配置 200 个屏蔽词"),
|
keywords: z.array(regionBlockKeywordSchema).max(200, "最多配置 200 个屏蔽词"),
|
||||||
|
whitelist_ips: z.array(regionBlockIPSchema).max(500, "最多配置 500 个白名单 IP"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RegionBlockReplaceForm = z.infer<typeof regionBlockReplaceSchema>;
|
export type RegionBlockReplaceForm = z.infer<typeof regionBlockReplaceSchema>;
|
||||||
|
|
||||||
|
function isIPv4(value: string) {
|
||||||
|
const parts = value.split(".");
|
||||||
|
return (
|
||||||
|
parts.length === 4 &&
|
||||||
|
parts.every((part) => {
|
||||||
|
if (!/^\d{1,3}$/.test(part)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const number = Number(part);
|
||||||
|
return number >= 0 && number <= 255 && String(number) === part;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIPv6(value: string) {
|
||||||
|
return value.includes(":") && /^[0-9a-fA-F:.]+$/.test(value) && value.split(":").length >= 3;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user