feat: add domain rules management feature

- Introduced a new DomainRulesPage component for managing custom and global equivalent domains.
- Updated AppMainRoutes to include a route for domain rules.
- Added API functions to fetch and save domain rules.
- Enhanced localization with new strings for domain rules in multiple languages.
- Updated styles for the new domain rules interface and ensured responsiveness.
- Added types for domain rules in the TypeScript definitions.
This commit is contained in:
shuaiplus
2026-05-06 00:33:09 +08:00
parent 246c73a3d3
commit 0a001bebcc
32 changed files with 2045 additions and 32 deletions
+151
View File
@@ -0,0 +1,151 @@
const MULTI_LABEL_PUBLIC_SUFFIXES = new Set([
'ac.cn',
'com.cn',
'edu.cn',
'gov.cn',
'net.cn',
'org.cn',
'ah.cn',
'bj.cn',
'cq.cn',
'fj.cn',
'gd.cn',
'gs.cn',
'gx.cn',
'gz.cn',
'ha.cn',
'hb.cn',
'he.cn',
'hi.cn',
'hk.cn',
'hl.cn',
'hn.cn',
'jl.cn',
'js.cn',
'jx.cn',
'ln.cn',
'mo.cn',
'nm.cn',
'nx.cn',
'qh.cn',
'sc.cn',
'sd.cn',
'sh.cn',
'sn.cn',
'sx.cn',
'tj.cn',
'tw.cn',
'xj.cn',
'xz.cn',
'yn.cn',
'zj.cn',
'co.uk',
'org.uk',
'net.uk',
'ac.uk',
'gov.uk',
'com.au',
'net.au',
'org.au',
'edu.au',
'gov.au',
'co.nz',
'org.nz',
'net.nz',
'com.br',
'com.mx',
'com.ar',
'com.tr',
'com.sg',
'com.my',
'com.hk',
'com.tw',
'co.jp',
'ne.jp',
'or.jp',
'co.kr',
'or.kr',
'co.in',
'firm.in',
'net.in',
'org.in',
'co.id',
'or.id',
'web.id',
'co.il',
'org.il',
'co.za',
'com.sa',
'com.ph',
'com.vn',
'com.pk',
'com.bd',
'com.ng',
'github.io',
'pages.dev',
'workers.dev',
'cloudflareaccess.com',
'vercel.app',
'netlify.app',
'web.app',
'firebaseapp.com',
'herokuapp.com',
'fly.dev',
'railway.app',
'render.com',
'onrender.com',
]);
function extractHost(input: string): string {
let raw = input.trim().toLowerCase();
if (!raw) return '';
raw = raw.replace(/\\/g, '/');
try {
const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
const parsed = new URL(candidate);
raw = parsed.hostname;
} catch {
raw = raw.split(/[/?#]/, 1)[0] || '';
const atIndex = raw.lastIndexOf('@');
if (atIndex >= 0) raw = raw.slice(atIndex + 1);
if (raw.startsWith('[')) return '';
const colonIndex = raw.lastIndexOf(':');
if (colonIndex > -1 && raw.indexOf(':') === colonIndex) raw = raw.slice(0, colonIndex);
}
return raw
.replace(/^\*+\./, '')
.replace(/^\.+/, '')
.replace(/\.+$/, '');
}
function isValidHost(host: string): boolean {
if (!host || host.length > 253 || !host.includes('.')) return false;
if (host.includes('..') || /[:/\s]/.test(host)) return false;
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return false;
return host.split('.').every((label) => (
label.length > 0
&& label.length <= 63
&& /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label)
));
}
export function normalizeEquivalentDomain(value: unknown): string {
const host = extractHost(String(value || ''));
if (!isValidHost(host)) return '';
const labels = host.split('.');
for (let index = 0; index < labels.length; index += 1) {
const suffix = labels.slice(index).join('.');
if (!MULTI_LABEL_PUBLIC_SUFFIXES.has(suffix)) continue;
if (index === 0) return '';
return labels.slice(index - 1).join('.');
}
return labels.length >= 2 ? labels.slice(-2).join('.') : '';
}
export function isValidEquivalentDomain(value: unknown): boolean {
return !!normalizeEquivalentDomain(value);
}