mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
feat: add firewall rule import/export with JSON validation and alerts (#10706)
This commit is contained in:
@@ -2872,6 +2872,19 @@ const message = {
|
||||
forwardHelper2: 'Leave the destination IP blank to forward to the local port.',
|
||||
forwardHelper3: 'Only support IPv4 port forwarding.',
|
||||
forwardInboundInterface: 'Forward Inbound Network Interface',
|
||||
importHelper:
|
||||
'Upload a JSON file to import firewall rules. The system will automatically compare and show differences',
|
||||
importNew: 'New rules',
|
||||
importConflict: 'Conflicting rules',
|
||||
importDuplicate: 'Duplicate rules',
|
||||
new: 'New',
|
||||
conflict: 'Conflict',
|
||||
duplicate: 'Duplicate',
|
||||
errImportFormat: 'Import file format error, please check the JSON file format',
|
||||
errImport: 'Import failed: ',
|
||||
selectImportRules: 'Please select rules to import',
|
||||
importSuccess: 'Successfully imported {0} rules',
|
||||
importPartialSuccess: 'Import completed: {0} succeeded, {1} failed',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Runtime',
|
||||
|
||||
@@ -2841,6 +2841,19 @@ const message = {
|
||||
forwardHelper2: 'Deja en blanco la IP de destino para reenviar al puerto local.',
|
||||
forwardHelper3: 'Solo se admite redirección de puertos IPv4.',
|
||||
forwardInboundInterface: 'Interfaz de Red de Entrada para Reenvío',
|
||||
importHelper:
|
||||
'Sube un archivo JSON para importar reglas del firewall. El sistema comparará automáticamente y mostrará las diferencias',
|
||||
importNew: 'Reglas nuevas',
|
||||
importConflict: 'Reglas en conflicto',
|
||||
importDuplicate: 'Reglas duplicadas',
|
||||
new: 'Nueva',
|
||||
conflict: 'Conflicto',
|
||||
duplicate: 'Duplicada',
|
||||
errImportFormat: 'Error en el formato del archivo importado, comprueba el formato del archivo JSON',
|
||||
errImport: 'Error al importar: ',
|
||||
selectImportRules: 'Selecciona las reglas que deseas importar',
|
||||
importSuccess: 'Se importaron correctamente {0} reglas',
|
||||
importPartialSuccess: 'Importación completada: {0} correctas, {1} fallidas',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Runtime',
|
||||
|
||||
@@ -2785,6 +2785,19 @@ const message = {
|
||||
forwardHelper2: '宛先IPを空白のままにして、ローカルポートに転送します。',
|
||||
forwardHelper3: 'IPv4ポート転送のみをサポートします。',
|
||||
forwardInboundInterface: '転送入站ネットワークインターフェース',
|
||||
importHelper:
|
||||
'JSON ファイルをアップロードしてファイアウォールルールをインポートします。システムが自動で比較し、差分を表示します',
|
||||
importNew: '新規ルール',
|
||||
importConflict: '競合ルール',
|
||||
importDuplicate: '重複ルール',
|
||||
new: '新規',
|
||||
conflict: '競合',
|
||||
duplicate: '重複',
|
||||
errImportFormat: 'インポートファイルの形式エラーです。JSON ファイルの形式を確認してください',
|
||||
errImport: 'インポートに失敗しました: ',
|
||||
selectImportRules: 'インポートするルールを選択してください',
|
||||
importSuccess: '{0} 件のルールを正常にインポートしました',
|
||||
importPartialSuccess: 'インポート完了: {0} 件成功、{1} 件失敗',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'ランタイム',
|
||||
|
||||
@@ -2734,6 +2734,18 @@ const message = {
|
||||
forwardHelper2: '대상 IP 를 비워두면 로컬 포트로 전달됩니다.',
|
||||
forwardHelper3: 'IPv4 포트 전달만 지원됩니다.',
|
||||
forwardInboundInterface: '포워딩 인바운드 네트워크 인터페이스',
|
||||
importHelper: 'JSON 파일을 업로드하여 방화벽 규칙을 가져옵니다. 시스템이 자동으로 비교하고 차이를 표시합니다',
|
||||
importNew: '새 규칙',
|
||||
importConflict: '충돌 규칙',
|
||||
importDuplicate: '중복 규칙',
|
||||
new: '신규',
|
||||
conflict: '충돌',
|
||||
duplicate: '중복',
|
||||
errImportFormat: '가져오기 파일 형식 오류입니다. JSON 파일 형식을 확인하세요',
|
||||
errImport: '가져오기에 실패했습니다: ',
|
||||
selectImportRules: '가져올 규칙을 선택하세요',
|
||||
importSuccess: '{0}개의 규칙을 성공적으로 가져왔습니다',
|
||||
importPartialSuccess: '가져오기 완료: 성공 {0}건, 실패 {1}건',
|
||||
},
|
||||
runtime: {
|
||||
runtime: '실행 환경',
|
||||
|
||||
@@ -2849,6 +2849,19 @@ const message = {
|
||||
forwardHelper2: 'Biarkan IP sasaran kosong untuk memajukan ke port tempatan.',
|
||||
forwardHelper3: 'Hanya menyokong pemajuan port IPv4.',
|
||||
forwardInboundInterface: 'Antara Muka Rangkaian Masukan Penerusan',
|
||||
importHelper:
|
||||
'Muat naik fail JSON untuk mengimport peraturan firewall. Sistem akan membanding dan memaparkan perbezaan secara automatik',
|
||||
importNew: 'Peraturan baharu',
|
||||
importConflict: 'Peraturan konflik',
|
||||
importDuplicate: 'Peraturan duplikat',
|
||||
new: 'Baharu',
|
||||
conflict: 'Konflik',
|
||||
duplicate: 'Duplikat',
|
||||
errImportFormat: 'Ralat format fail import, sila semak format fail JSON',
|
||||
errImport: 'Import gagal: ',
|
||||
selectImportRules: 'Sila pilih peraturan untuk diimport',
|
||||
importSuccess: '{0} peraturan berjaya diimport',
|
||||
importPartialSuccess: 'Import selesai: {0} berjaya, {1} gagal',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Runtime',
|
||||
|
||||
@@ -2855,6 +2855,19 @@ const message = {
|
||||
forwardHelper2: 'Deixe o IP de destino em branco para redirecionar para a porta local.',
|
||||
forwardHelper3: 'Somente suporta redirecionamento de porta IPv4.',
|
||||
forwardInboundInterface: 'Interface de Rede de Entrada para Encaminhamento',
|
||||
importHelper:
|
||||
'Envie um arquivo JSON para importar as regras do firewall. O sistema irá comparar e mostrar as diferenças automaticamente',
|
||||
importNew: 'Novas regras',
|
||||
importConflict: 'Regras em conflito',
|
||||
importDuplicate: 'Regras duplicadas',
|
||||
new: 'Nova',
|
||||
conflict: 'Conflito',
|
||||
duplicate: 'Duplicada',
|
||||
errImportFormat: 'Erro no formato do arquivo de importação, verifique o formato do arquivo JSON',
|
||||
errImport: 'Falha na importação: ',
|
||||
selectImportRules: 'Selecione as regras que deseja importar',
|
||||
importSuccess: '{0} regras importadas com sucesso',
|
||||
importPartialSuccess: 'Importação concluída: {0} sucesso, {1} falha',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Runtime',
|
||||
|
||||
@@ -2851,6 +2851,19 @@ const message = {
|
||||
forwardHelper2: 'Оставьте целевой IP пустым для перенаправления на локальный порт.',
|
||||
forwardHelper3: 'Поддерживается только переадресация портов IPv4.',
|
||||
forwardInboundInterface: '转发入站Сетевой интерфейс для пересылки входящего трафика网卡',
|
||||
importHelper:
|
||||
'Загрузите JSON-файл, чтобы импортировать правила брандмауэра. Система автоматически сравнит их и покажет различия',
|
||||
importNew: 'Новые правила',
|
||||
importConflict: 'Конфликтующие правила',
|
||||
importDuplicate: 'Дублирующиеся правила',
|
||||
new: 'Новое',
|
||||
conflict: 'Конфликт',
|
||||
duplicate: 'Дубликат',
|
||||
errImportFormat: 'Ошибка формата импортируемого файла, проверьте формат JSON',
|
||||
errImport: 'Не удалось импортировать: ',
|
||||
selectImportRules: 'Выберите правила для импорта',
|
||||
importSuccess: 'Успешно импортировано {0} правил',
|
||||
importPartialSuccess: 'Импорт завершён: {0} успешно, {1} с ошибкой',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Среда выполнения',
|
||||
|
||||
@@ -2912,6 +2912,19 @@ const message = {
|
||||
forwardHelper2: 'Yerel porta yönlendirmek için hedef IP’yi boş bırakın.',
|
||||
forwardHelper3: 'Yalnızca IPv4 port yönlendirmesini destekler.',
|
||||
forwardInboundInterface: 'İletme Gelen Ağ Arayüzü',
|
||||
importHelper:
|
||||
'Güvenlik duvarı kurallarını içe aktarmak için bir JSON dosyası yükleyin. Sistem otomatik olarak karşılaştırıp farkları gösterecektir',
|
||||
importNew: 'Yeni kurallar',
|
||||
importConflict: 'Çakışan kurallar',
|
||||
importDuplicate: 'Yinelenen kurallar',
|
||||
new: 'Yeni',
|
||||
conflict: 'Çakışma',
|
||||
duplicate: 'Yinelenen',
|
||||
errImportFormat: 'İçe aktarma dosya biçimi hatalı, lütfen JSON dosya biçimini kontrol edin',
|
||||
errImport: 'İçe aktarma başarısız: ',
|
||||
selectImportRules: 'Lütfen içe aktarılacak kuralları seçin',
|
||||
importSuccess: '{0} kural başarıyla içe aktarıldı',
|
||||
importPartialSuccess: 'İçe aktarma tamamlandı: {0} başarılı, {1} başarısız',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Çalışma Zamanı',
|
||||
|
||||
@@ -2668,6 +2668,18 @@ const message = {
|
||||
forwardHelper2: '如果目標 IP 不填寫,預設為本機埠轉發',
|
||||
forwardHelper3: '目前僅支援 IPv4 的埠轉發',
|
||||
forwardInboundInterface: '轉發入站網路介面',
|
||||
importHelper: '上傳 JSON 檔案以匯入防火牆規則,系統會自動比對並顯示差異',
|
||||
importNew: '新增規則',
|
||||
importConflict: '衝突規則',
|
||||
importDuplicate: '重複規則',
|
||||
new: '新增',
|
||||
conflict: '衝突',
|
||||
duplicate: '重複',
|
||||
errImportFormat: '匯入檔案格式錯誤,請檢查 JSON 檔案格式',
|
||||
errImport: '匯入失敗:',
|
||||
selectImportRules: '請選擇要匯入的規則',
|
||||
importSuccess: '成功匯入 {0} 條規則',
|
||||
importPartialSuccess: '匯入完成:成功 {0} 條,失敗 {1} 條',
|
||||
},
|
||||
runtime: {
|
||||
runtime: '執行環境',
|
||||
|
||||
@@ -2661,6 +2661,18 @@ const message = {
|
||||
forwardHelper2: '如果目标IP不填写,则默认为本机端口转发',
|
||||
forwardHelper3: '当前仅支持 IPv4 的端口转发',
|
||||
forwardInboundInterface: '转发入站网卡',
|
||||
importHelper: '上传 JSON 文件以导入防火墙规则,系统会自动比对并显示差异',
|
||||
importNew: '新增规则',
|
||||
importConflict: '冲突规则',
|
||||
importDuplicate: '重复规则',
|
||||
new: '新规则',
|
||||
conflict: '冲突',
|
||||
duplicate: '重复',
|
||||
errImportFormat: '导入文件格式错误,请检查 JSON 文件格式',
|
||||
errImport: '导入失败:',
|
||||
selectImportRules: '请选择要导入的规则',
|
||||
importSuccess: '成功导入 {0} 条规则',
|
||||
importPartialSuccess: '导入完成:成功 {0} 条,失败 {1} 条',
|
||||
},
|
||||
runtime: {
|
||||
runtime: '运行环境',
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
|
||||
<div>
|
||||
<el-alert :closable="false" show-icon type="info">
|
||||
<template #default>
|
||||
<div>{{ $t('firewall.importHelper') }}</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadRef"
|
||||
class="float-left mt-2"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="fileOnChange"
|
||||
:on-exceed="handleExceed"
|
||||
v-model:file-list="uploaderFiles"
|
||||
>
|
||||
<el-button class="float-left" type="primary">{{ $t('commons.button.upload') }}</el-button>
|
||||
</el-upload>
|
||||
|
||||
<el-card class="mt-2 w-full" v-loading="loading">
|
||||
<div v-if="compareResult.new.length > 0 || compareResult.conflict.length > 0">
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="success"
|
||||
class="mb-2"
|
||||
v-if="compareResult.new.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importNew') }}:
|
||||
<strong>{{ compareResult.new.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="warning"
|
||||
class="mb-2"
|
||||
v-if="compareResult.conflict.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importConflict') }}:
|
||||
<strong>{{ compareResult.conflict.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="info"
|
||||
class="mb-2"
|
||||
v-if="compareResult.duplicate.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importDuplicate') }}:
|
||||
<strong>{{ compareResult.duplicate.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<el-table :data="displayData" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.status')" :min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'new'" type="success">{{ $t('firewall.new') }}</el-tag>
|
||||
<el-tag v-else-if="row.status === 'conflict'" type="warning">
|
||||
{{ $t('firewall.conflict') }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info">{{ $t('firewall.duplicate') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.protocol')" :min-width="70" prop="protocol" />
|
||||
<el-table-column :label="$t('firewall.sourcePort')" :min-width="70" prop="port" />
|
||||
<el-table-column :label="$t('firewall.targetIP')" :min-width="100" prop="targetIP" />
|
||||
<el-table-column :label="$t('firewall.targetPort')" :min-width="70" prop="targetPort" />
|
||||
<el-table-column
|
||||
v-if="currentFireName === 'ufw'"
|
||||
:label="$t('firewall.forwardInboundInterface')"
|
||||
:min-width="100"
|
||||
prop="interface"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>
|
||||
{{
|
||||
row.interface === '' || row.interface === 'all'
|
||||
? $t('commons.table.all')
|
||||
: row.interface
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="selects.length === 0" @click="onImport">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { genFileId, UploadFile, UploadFiles, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import { operateForwardRule, searchFireRule, getNetworkOptions } from '@/api/modules/host';
|
||||
import { Host } from '@/api/interface/host';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const selects = ref<any>([]);
|
||||
const displayData = ref<any>([]);
|
||||
const currentRules = ref<Host.RuleInfo[]>([]);
|
||||
const currentFireName = ref('');
|
||||
const availableInterfaces = ref<string[]>([]);
|
||||
|
||||
const uploadRef = ref();
|
||||
const uploaderFiles = ref();
|
||||
|
||||
interface CompareResult {
|
||||
new: any[];
|
||||
conflict: any[];
|
||||
duplicate: any[];
|
||||
}
|
||||
|
||||
const compareResult = ref<CompareResult>({
|
||||
new: [],
|
||||
conflict: [],
|
||||
duplicate: [],
|
||||
});
|
||||
|
||||
const acceptParams = async (fireName: string): Promise<void> => {
|
||||
visible.value = true;
|
||||
displayData.value = [];
|
||||
selects.value = [];
|
||||
compareResult.value = { new: [], conflict: [], duplicate: [] };
|
||||
currentFireName.value = fireName;
|
||||
|
||||
// Fetch all current rules for comparison
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await searchFireRule({
|
||||
type: 'forward',
|
||||
status: '',
|
||||
strategy: '',
|
||||
info: '',
|
||||
page: 1,
|
||||
pageSize: 10000, // Fetch all rules
|
||||
});
|
||||
currentRules.value = res.data.items || [];
|
||||
|
||||
// Fetch available network interfaces (UFW only)
|
||||
if (fireName === 'ufw') {
|
||||
const networkRes = await getNetworkOptions();
|
||||
availableInterfaces.value = networkRes.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('commons.msg.searchFailed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
selects.value = val;
|
||||
};
|
||||
|
||||
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
loading.value = true;
|
||||
displayData.value = [];
|
||||
compareResult.value = { new: [], conflict: [], duplicate: [] };
|
||||
uploaderFiles.value = uploadFiles;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target.result as string;
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
MsgError(i18n.global.t('firewall.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of parsed) {
|
||||
if (!checkDataFormat(item)) {
|
||||
MsgError(i18n.global.t('firewall.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
compareRules(parsed);
|
||||
loading.value = false;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('firewall.errImport') + error.message);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
const checkDataFormat = (item: any): boolean => {
|
||||
if (!item.protocol || !item.port || !item.targetIP || !item.targetPort) {
|
||||
return false;
|
||||
}
|
||||
if (!['tcp', 'udp', 'tcp/udp'].includes(item.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate network interface (UFW only)
|
||||
if (currentFireName.value === 'ufw' && item.interface !== undefined && item.interface !== null) {
|
||||
const interfaceValue = item.interface;
|
||||
// Allow empty string or 'all' (represents all interfaces)
|
||||
if (interfaceValue !== '' && interfaceValue !== 'all') {
|
||||
// Must be in available interfaces list
|
||||
if (!availableInterfaces.value.includes(interfaceValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const compareRules = (importedRules: any[]) => {
|
||||
const newRules: any[] = [];
|
||||
const conflictRules: any[] = [];
|
||||
const duplicateRules: any[] = [];
|
||||
|
||||
for (const importedRule of importedRules) {
|
||||
const key = `${importedRule.protocol}:${importedRule.port}:${importedRule.targetIP}:${importedRule.targetPort}`;
|
||||
|
||||
const existingRule = currentRules.value.find((rule) => {
|
||||
const existingKey = `${rule.protocol}:${rule.port}:${rule.targetIP}:${rule.targetPort}`;
|
||||
return existingKey === key;
|
||||
});
|
||||
|
||||
if (!existingRule) {
|
||||
newRules.push({ ...importedRule, status: 'new' });
|
||||
} else {
|
||||
duplicateRules.push({ ...importedRule, status: 'duplicate' });
|
||||
}
|
||||
}
|
||||
|
||||
compareResult.value = {
|
||||
new: newRules,
|
||||
conflict: conflictRules,
|
||||
duplicate: duplicateRules,
|
||||
};
|
||||
|
||||
displayData.value = [...newRules, ...conflictRules, ...duplicateRules];
|
||||
};
|
||||
|
||||
const onImport = async () => {
|
||||
if (selects.value.length === 0) {
|
||||
MsgError(i18n.global.t('firewall.selectImportRules'));
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
const rules: Host.RuleForward[] = [];
|
||||
for (const rule of selects.value) {
|
||||
rules.push({
|
||||
operation: 'add',
|
||||
protocol: rule.protocol,
|
||||
port: rule.port,
|
||||
targetIP: rule.targetIP,
|
||||
targetPort: rule.targetPort,
|
||||
interface: rule.interface || '',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await operateForwardRule({ rules });
|
||||
successCount = rules.length;
|
||||
} catch (error) {
|
||||
errorCount = rules.length;
|
||||
console.error('Failed to import rules:', error);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (errorCount === 0) {
|
||||
MsgSuccess(i18n.global.t('firewall.importSuccess', [successCount]));
|
||||
visible.value = false;
|
||||
emit('search');
|
||||
} else {
|
||||
MsgError(i18n.global.t('firewall.importPartialSuccess', [successCount, errorCount]));
|
||||
emit('search');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
@@ -24,6 +24,14 @@
|
||||
<el-button @click="onDelete(null)" plain :disabled="selects.length === 0">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
<el-button-group>
|
||||
<el-button @click="onImport">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
<el-button :disabled="!data || data.length === 0" @click="onExport">
|
||||
{{ $t('commons.button.export') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
@@ -82,18 +90,21 @@
|
||||
</template>
|
||||
</OpDialog>
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<ImportDialog @search="search" ref="dialogImportRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FireRouter from '@/views/host/firewall/index.vue';
|
||||
import OperateDialog from './operate/index.vue';
|
||||
import ImportDialog from './import/index.vue';
|
||||
import FireStatus from '@/views/host/firewall/status/index.vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { operateForwardRule, searchFireRule } from '@/api/modules/host';
|
||||
import { Host } from '@/api/interface/host';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { downloadWithContent, getCurrentDateFormatted } from '@/utils/util';
|
||||
|
||||
const loading = ref();
|
||||
const activeTag = ref('forward');
|
||||
@@ -108,6 +119,7 @@ const fireName = ref();
|
||||
const fireStatusRef = ref();
|
||||
|
||||
const opRef = ref();
|
||||
const dialogImportRef = ref();
|
||||
const forceDelete = ref(false);
|
||||
const operateRules = ref();
|
||||
|
||||
@@ -213,6 +225,24 @@ const onSubmitDelete = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onImport = () => {
|
||||
dialogImportRef.value.acceptParams(fireName.value);
|
||||
};
|
||||
|
||||
const onExport = () => {
|
||||
const exportData = data.value.map((item: Host.RuleInfo) => ({
|
||||
family: item.family,
|
||||
protocol: item.protocol,
|
||||
port: item.port,
|
||||
targetIP: item.targetIP,
|
||||
targetPort: item.targetPort,
|
||||
interface: item.interface,
|
||||
}));
|
||||
const content = JSON.stringify(exportData, null, 2);
|
||||
const fileName = `1panel-firewall-forward-${getCurrentDateFormatted()}.json`;
|
||||
downloadWithContent(content, fileName);
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
|
||||
<div>
|
||||
<el-alert :closable="false" show-icon type="info">
|
||||
<template #default>
|
||||
<div>{{ $t('firewall.importHelper') }}</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadRef"
|
||||
class="float-left mt-2"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="fileOnChange"
|
||||
:on-exceed="handleExceed"
|
||||
v-model:file-list="uploaderFiles"
|
||||
>
|
||||
<el-button class="float-left" type="primary">{{ $t('commons.button.upload') }}</el-button>
|
||||
</el-upload>
|
||||
|
||||
<el-card class="mt-2 w-full" v-loading="loading">
|
||||
<div v-if="compareResult.new.length > 0 || compareResult.conflict.length > 0">
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="success"
|
||||
class="mb-2"
|
||||
v-if="compareResult.new.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importNew') }}:
|
||||
<strong>{{ compareResult.new.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="warning"
|
||||
class="mb-2"
|
||||
v-if="compareResult.conflict.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importConflict') }}:
|
||||
<strong>{{ compareResult.conflict.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="info"
|
||||
class="mb-2"
|
||||
v-if="compareResult.duplicate.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importDuplicate') }}:
|
||||
<strong>{{ compareResult.duplicate.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<el-table :data="displayData" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.status')" :min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'new'" type="success">{{ $t('firewall.new') }}</el-tag>
|
||||
<el-tag v-else-if="row.status === 'conflict'" type="warning">
|
||||
{{ $t('firewall.conflict') }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info">{{ $t('firewall.duplicate') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('firewall.address')" :min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.address && row.address !== 'Anywhere'">{{ row.address }}</span>
|
||||
<span v-else>{{ $t('firewall.allIP') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('firewall.strategy')" :min-width="80" prop="strategy">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.strategy === 'accept'" type="success">{{ $t('firewall.allow') }}</el-tag>
|
||||
<el-tag v-else type="danger">{{ $t('firewall.deny') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.description')"
|
||||
:min-width="120"
|
||||
prop="description"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="selects.length === 0" @click="onImport">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { genFileId, UploadFile, UploadFiles, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import { operateIPRule, searchFireRule } from '@/api/modules/host';
|
||||
import { Host } from '@/api/interface/host';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const selects = ref<any>([]);
|
||||
const displayData = ref<any>([]);
|
||||
const currentRules = ref<Host.RuleInfo[]>([]);
|
||||
|
||||
const uploadRef = ref();
|
||||
const uploaderFiles = ref();
|
||||
|
||||
interface CompareResult {
|
||||
new: any[];
|
||||
conflict: any[];
|
||||
duplicate: any[];
|
||||
}
|
||||
|
||||
const compareResult = ref<CompareResult>({
|
||||
new: [],
|
||||
conflict: [],
|
||||
duplicate: [],
|
||||
});
|
||||
|
||||
const acceptParams = async (): Promise<void> => {
|
||||
visible.value = true;
|
||||
displayData.value = [];
|
||||
selects.value = [];
|
||||
compareResult.value = { new: [], conflict: [], duplicate: [] };
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await searchFireRule({
|
||||
type: 'address',
|
||||
status: '',
|
||||
strategy: '',
|
||||
info: '',
|
||||
page: 1,
|
||||
pageSize: 10000,
|
||||
});
|
||||
currentRules.value = res.data.items || [];
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('commons.msg.searchFailed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
selects.value = val;
|
||||
};
|
||||
|
||||
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
loading.value = true;
|
||||
displayData.value = [];
|
||||
compareResult.value = { new: [], conflict: [], duplicate: [] };
|
||||
uploaderFiles.value = uploadFiles;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target.result as string;
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
MsgError(i18n.global.t('firewall.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of parsed) {
|
||||
if (!checkDataFormat(item)) {
|
||||
MsgError(i18n.global.t('firewall.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
compareRules(parsed);
|
||||
loading.value = false;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('firewall.errImport') + error.message);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
const checkDataFormat = (item: any): boolean => {
|
||||
if (!item.address || !item.strategy) {
|
||||
return false;
|
||||
}
|
||||
if (!['accept', 'drop'].includes(item.strategy)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const compareRules = (importedRules: any[]) => {
|
||||
const newRules: any[] = [];
|
||||
const conflictRules: any[] = [];
|
||||
const duplicateRules: any[] = [];
|
||||
|
||||
for (const importedRule of importedRules) {
|
||||
const key = `${importedRule.address || 'Anywhere'}`;
|
||||
|
||||
const existingRule = currentRules.value.find((rule) => {
|
||||
const existingKey = `${rule.address || 'Anywhere'}`;
|
||||
return existingKey === key;
|
||||
});
|
||||
|
||||
if (!existingRule) {
|
||||
newRules.push({ ...importedRule, status: 'new' });
|
||||
} else if (existingRule.strategy !== importedRule.strategy) {
|
||||
conflictRules.push({
|
||||
...importedRule,
|
||||
status: 'conflict',
|
||||
existingStrategy: existingRule.strategy,
|
||||
});
|
||||
} else {
|
||||
duplicateRules.push({ ...importedRule, status: 'duplicate' });
|
||||
}
|
||||
}
|
||||
|
||||
compareResult.value = {
|
||||
new: newRules,
|
||||
conflict: conflictRules,
|
||||
duplicate: duplicateRules,
|
||||
};
|
||||
|
||||
displayData.value = [...newRules, ...conflictRules, ...duplicateRules];
|
||||
};
|
||||
|
||||
const onImport = async () => {
|
||||
if (selects.value.length === 0) {
|
||||
MsgError(i18n.global.t('firewall.selectImportRules'));
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const rule of selects.value) {
|
||||
try {
|
||||
const params: Host.RuleIP = {
|
||||
operation: 'add',
|
||||
address: rule.address || 'Anywhere',
|
||||
strategy: rule.strategy,
|
||||
description: rule.description || '',
|
||||
};
|
||||
|
||||
await operateIPRule(params);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.error('Failed to import rule:', rule, error);
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (errorCount === 0) {
|
||||
MsgSuccess(i18n.global.t('firewall.importSuccess', [successCount]));
|
||||
visible.value = false;
|
||||
emit('search');
|
||||
} else {
|
||||
MsgError(i18n.global.t('firewall.importPartialSuccess', [successCount, errorCount]));
|
||||
emit('search');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
@@ -25,6 +25,14 @@
|
||||
<el-button @click="onDelete(null)" plain :disabled="selects.length === 0">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
<el-button-group>
|
||||
<el-button @click="onImport">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
<el-button :disabled="!data || data.length === 0" @click="onExport">
|
||||
{{ $t('commons.button.export') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select v-model="searchStrategy" @change="search()" clearable class="p-w-200">
|
||||
@@ -92,11 +100,13 @@
|
||||
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<ImportDialog @search="search" ref="dialogImportRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import OperateDialog from '@/views/host/firewall/ip/operate/index.vue';
|
||||
import ImportDialog from '@/views/host/firewall/ip/import/index.vue';
|
||||
import FireRouter from '@/views/host/firewall/index.vue';
|
||||
import FireStatus from '@/views/host/firewall/status/index.vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
@@ -105,6 +115,7 @@ import { Host } from '@/api/interface/host';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { downloadWithContent, getCurrentDateFormatted } from '@/utils/util';
|
||||
|
||||
const loading = ref();
|
||||
const activeTag = ref('address');
|
||||
@@ -118,6 +129,7 @@ const isActive = ref(false);
|
||||
const fireStatusRef = ref();
|
||||
|
||||
const opRef = ref();
|
||||
const dialogImportRef = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
@@ -248,6 +260,22 @@ const onDelete = async (row: Host.RuleIP | null) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onImport = () => {
|
||||
dialogImportRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const onExport = () => {
|
||||
const exportData = data.value.map((item: Host.RuleInfo) => ({
|
||||
family: item.family,
|
||||
address: item.address,
|
||||
strategy: item.strategy,
|
||||
description: item.description,
|
||||
}));
|
||||
const content = JSON.stringify(exportData, null, 2);
|
||||
const fileName = `1panel-firewall-ip-${getCurrentDateFormatted()}.json`;
|
||||
downloadWithContent(content, fileName);
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
|
||||
<div>
|
||||
<el-alert :closable="false" show-icon type="info">
|
||||
<template #default>
|
||||
<div>{{ $t('firewall.importHelper') }}</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadRef"
|
||||
class="float-left mt-2"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="fileOnChange"
|
||||
:on-exceed="handleExceed"
|
||||
v-model:file-list="uploaderFiles"
|
||||
>
|
||||
<el-button class="float-left" type="primary">{{ $t('commons.button.upload') }}</el-button>
|
||||
</el-upload>
|
||||
|
||||
<el-card class="mt-2 w-full" v-loading="loading">
|
||||
<div v-if="compareResult.new.length > 0 || compareResult.conflict.length > 0">
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="success"
|
||||
class="mb-2"
|
||||
v-if="compareResult.new.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importNew') }}:
|
||||
<strong>{{ compareResult.new.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="warning"
|
||||
class="mb-2"
|
||||
v-if="compareResult.conflict.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importConflict') }}:
|
||||
<strong>{{ compareResult.conflict.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="info"
|
||||
class="mb-2"
|
||||
v-if="compareResult.duplicate.length > 0"
|
||||
>
|
||||
<template #default>
|
||||
<span>
|
||||
{{ $t('firewall.importDuplicate') }}:
|
||||
<strong>{{ compareResult.duplicate.length }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<el-table :data="displayData" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.status')" :min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'new'" type="success">{{ $t('firewall.new') }}</el-tag>
|
||||
<el-tag v-else-if="row.status === 'conflict'" type="warning">
|
||||
{{ $t('firewall.conflict') }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info">{{ $t('firewall.duplicate') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.protocol')" :min-width="70" prop="protocol" />
|
||||
<el-table-column :label="$t('commons.table.port')" :min-width="70" prop="port" />
|
||||
<el-table-column :label="$t('firewall.address')" :min-width="80">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.address && row.address !== 'Anywhere'">{{ row.address }}</span>
|
||||
<span v-else>{{ $t('firewall.allIP') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('firewall.strategy')" :min-width="80" prop="strategy">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.strategy === 'accept'" type="success">{{ $t('firewall.accept') }}</el-tag>
|
||||
<el-tag v-else type="danger">{{ $t('firewall.drop') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.description')"
|
||||
:min-width="120"
|
||||
prop="description"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="selects.length === 0" @click="onImport">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { genFileId, UploadFile, UploadFiles, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import { operatePortRule, searchFireRule } from '@/api/modules/host';
|
||||
import { Host } from '@/api/interface/host';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const selects = ref<any>([]);
|
||||
const displayData = ref<any>([]);
|
||||
const currentRules = ref<Host.RuleInfo[]>([]);
|
||||
|
||||
const uploadRef = ref();
|
||||
const uploaderFiles = ref();
|
||||
|
||||
interface CompareResult {
|
||||
new: any[];
|
||||
conflict: any[];
|
||||
duplicate: any[];
|
||||
}
|
||||
|
||||
const compareResult = ref<CompareResult>({
|
||||
new: [],
|
||||
conflict: [],
|
||||
duplicate: [],
|
||||
});
|
||||
|
||||
const acceptParams = async (): Promise<void> => {
|
||||
visible.value = true;
|
||||
displayData.value = [];
|
||||
selects.value = [];
|
||||
compareResult.value = { new: [], conflict: [], duplicate: [] };
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await searchFireRule({
|
||||
type: 'port',
|
||||
status: '',
|
||||
strategy: '',
|
||||
info: '',
|
||||
page: 1,
|
||||
pageSize: 10000,
|
||||
});
|
||||
currentRules.value = res.data.items || [];
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('commons.msg.searchFailed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
selects.value = val;
|
||||
};
|
||||
|
||||
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
loading.value = true;
|
||||
displayData.value = [];
|
||||
compareResult.value = { new: [], conflict: [], duplicate: [] };
|
||||
uploaderFiles.value = uploadFiles;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target.result as string;
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
MsgError(i18n.global.t('firewall.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of parsed) {
|
||||
if (!checkDataFormat(item)) {
|
||||
MsgError(i18n.global.t('firewall.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
compareRules(parsed);
|
||||
loading.value = false;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('firewall.errImport') + error.message);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
const checkDataFormat = (item: any): boolean => {
|
||||
if (!item.port || !item.protocol || !item.strategy) {
|
||||
return false;
|
||||
}
|
||||
if (!['tcp', 'udp', 'tcp/udp'].includes(item.protocol)) {
|
||||
return false;
|
||||
}
|
||||
if (!['accept', 'drop'].includes(item.strategy)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const compareRules = (importedRules: any[]) => {
|
||||
const newRules: any[] = [];
|
||||
const conflictRules: any[] = [];
|
||||
const duplicateRules: any[] = [];
|
||||
|
||||
for (const importedRule of importedRules) {
|
||||
const key = `${importedRule.address || 'Anywhere'}:${importedRule.port}:${importedRule.protocol}`;
|
||||
|
||||
const existingRule = currentRules.value.find((rule) => {
|
||||
const existingKey = `${rule.address || 'Anywhere'}:${rule.port}:${rule.protocol}`;
|
||||
return existingKey === key;
|
||||
});
|
||||
|
||||
if (!existingRule) {
|
||||
newRules.push({ ...importedRule, status: 'new' });
|
||||
} else if (existingRule.strategy !== importedRule.strategy) {
|
||||
conflictRules.push({
|
||||
...importedRule,
|
||||
status: 'conflict',
|
||||
existingStrategy: existingRule.strategy,
|
||||
});
|
||||
} else {
|
||||
duplicateRules.push({ ...importedRule, status: 'duplicate' });
|
||||
}
|
||||
}
|
||||
|
||||
compareResult.value = {
|
||||
new: newRules,
|
||||
conflict: conflictRules,
|
||||
duplicate: duplicateRules,
|
||||
};
|
||||
|
||||
displayData.value = [...newRules, ...conflictRules, ...duplicateRules];
|
||||
};
|
||||
|
||||
const onImport = async () => {
|
||||
if (selects.value.length === 0) {
|
||||
MsgError(i18n.global.t('firewall.selectImportRules'));
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const rule of selects.value) {
|
||||
try {
|
||||
const params: Host.RulePort = {
|
||||
operation: 'add',
|
||||
address: rule.address || 'Anywhere',
|
||||
port: rule.port,
|
||||
source: '',
|
||||
protocol: rule.protocol,
|
||||
strategy: rule.strategy,
|
||||
description: rule.description || '',
|
||||
};
|
||||
|
||||
await operatePortRule(params);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.error('Failed to import rule:', rule, error);
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (errorCount === 0) {
|
||||
MsgSuccess(i18n.global.t('firewall.importSuccess', [successCount]));
|
||||
visible.value = false;
|
||||
emit('search');
|
||||
} else {
|
||||
MsgError(i18n.global.t('firewall.importPartialSuccess', [successCount, errorCount]));
|
||||
emit('search');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
@@ -41,6 +41,14 @@
|
||||
<el-button @click="onDelete(null)" plain :disabled="selects.length === 0">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
<el-button-group>
|
||||
<el-button @click="onImport">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
<el-button :disabled="!data || data.length === 0" @click="onExport">
|
||||
{{ $t('commons.button.export') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select v-model="searchStatus" @change="search()" clearable class="p-w-200">
|
||||
@@ -142,12 +150,14 @@
|
||||
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<ImportDialog @search="search" ref="dialogImportRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FireRouter from '@/views/host/firewall/index.vue';
|
||||
import OperateDialog from '@/views/host/firewall/port/operate/index.vue';
|
||||
import ImportDialog from '@/views/host/firewall/port/import/index.vue';
|
||||
import FireStatus from '@/views/host/firewall/status/index.vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { batchOperateRule, searchFireRule, updateFirewallDescription, updatePortRule } from '@/api/modules/host';
|
||||
@@ -156,6 +166,7 @@ import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { routerToName } from '@/utils/router';
|
||||
import { downloadWithContent, getCurrentDateFormatted } from '@/utils/util';
|
||||
|
||||
const loading = ref();
|
||||
const activeTag = ref('port');
|
||||
@@ -170,6 +181,7 @@ const fireName = ref();
|
||||
const fireStatusRef = ref();
|
||||
|
||||
const opRef = ref();
|
||||
const dialogImportRef = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
@@ -315,6 +327,24 @@ const onDelete = async (row: Host.RuleInfo | null) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onImport = () => {
|
||||
dialogImportRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const onExport = () => {
|
||||
const exportData = data.value.map((item: Host.RuleInfo) => ({
|
||||
family: item.family,
|
||||
address: item.address,
|
||||
port: item.port,
|
||||
protocol: item.protocol,
|
||||
strategy: item.strategy,
|
||||
description: item.description,
|
||||
}));
|
||||
const content = JSON.stringify(exportData, null, 2);
|
||||
const fileName = `1panel-firewall-port-${getCurrentDateFormatted()}.json`;
|
||||
downloadWithContent(content, fileName);
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
|
||||
Reference in New Issue
Block a user