Files
opsx-vpn/frontend/src/views/TunnelEditor.vue
T

189 lines
9.3 KiB
Vue

<template>
<el-card class="page-card" shadow="never" v-loading="loading">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center">
<b>{{ $t('tunnelEditor.title') }}</b>
<el-button @click="router.push('/tunnels')">{{ $t('tunnelEditor.backToList') }}</el-button>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom:18px"
:title="$t('tunnelEditor.sensitiveFieldHint')" />
<el-form v-if="form" label-width="170px" label-position="left" style="max-width:720px">
<el-form-item :label="$t('tunnelEditor.tunnelName')" required>
<el-input v-model="form.name" />
</el-form-item>
<el-form-item :label="$t('tunnelEditor.protocolType')">
<el-tag :color="protocolColor(protocol)" effect="dark" style="border:none">{{ $t(protocolDisplay(protocol)) }}</el-tag>
</el-form-item>
<el-divider v-if="isOpenVPN" content-position="left" class="config-section-divider">{{ $t('tunnelEditor.coreOptions') }}</el-divider>
<el-form-item v-for="field in coreFields" :key="field.key" :label="$t(field.label)" :required="field.required">
<el-input-number v-if="field.type === 'number'" v-model="form.config[field.key]" :min="0" :max="65535" />
<el-switch v-else-if="field.type === 'switch'" v-model="form.config[field.key]" />
<el-select v-else-if="field.type === 'select'" v-model="form.config[field.key]" style="width:100%">
<el-option v-for="option in field.options" :key="optionValue(option)" :label="$t(optionLabel(option))" :value="optionValue(option)" :disabled="option.disabled" />
</el-select>
<el-input v-else-if="field.type === 'password'" v-model="form.config[field.key]" type="password"
show-password :placeholder="field.placeholder || $t('tunnelEditor.keepOriginal')" />
<el-input v-else v-model="form.config[field.key]" :placeholder="field.placeholder || field.default" />
<div v-if="field.tip" class="field-tip">{{ $t(field.tip) }}</div>
</el-form-item>
<OpenVPNOIDCGuide v-if="isOpenVPN && form.config.auth_mode === 'oidc'" :config="form.config" :editing="originalAuthMode === 'oidc'" />
<template v-if="isOpenVPN">
<el-form-item :label="$t('tunnelEditor.trafficMode')">
<el-radio-group v-model="trafficMode">
<el-radio-button value="gateway">{{ $t('tunnelEditor.trafficGateway') }}</el-radio-button>
<el-radio-button value="static">{{ $t('tunnelEditor.trafficStatic') }}</el-radio-button>
</el-radio-group>
<div class="field-tip">{{ $t(`tunnelEditor.traffic${trafficMode[0].toUpperCase()}${trafficMode.slice(1)}Tip`) }}</div>
</el-form-item>
<el-form-item v-if="trafficMode === 'static'" :label="$t('protocols.openvpn.fields.push_routes.label')">
<RouteListInput ref="routeListRef" v-model="form.config.push_routes" />
<div class="field-tip">{{ $t('protocols.openvpn.fields.push_routes.tip') }}</div>
</el-form-item>
</template>
<el-collapse v-if="advancedFields.length" v-model="advancedOpen" class="advanced-options">
<el-collapse-item :title="$t('tunnelEditor.advancedOptions')" name="advanced">
<el-form-item v-for="field in advancedFields" :key="field.key" :label="$t(field.label)" :required="field.required">
<el-input-number v-if="field.type === 'number'" v-model="form.config[field.key]" :min="0" :max="65535" />
<el-switch v-else-if="field.type === 'switch'" v-model="form.config[field.key]" />
<el-select v-else-if="field.type === 'select'" v-model="form.config[field.key]" style="width:100%">
<el-option v-for="option in field.options" :key="optionValue(option)" :label="$t(optionLabel(option))" :value="optionValue(option)" :disabled="option.disabled" />
</el-select>
<el-input v-else-if="field.type === 'password'" v-model="form.config[field.key]" type="password"
show-password :placeholder="field.placeholder || $t('tunnelEditor.keepOriginal')" />
<el-input v-else v-model="form.config[field.key]" :placeholder="field.placeholder || field.default" />
<div v-if="field.tip" class="field-tip">{{ $t(field.tip) }}</div>
</el-form-item>
</el-collapse-item>
</el-collapse>
<el-form-item :label="$t('tunnelEditor.expireTime')">
<el-date-picker v-model="form.expire_at" type="datetime" clearable :placeholder="$t('tunnelEditor.expirePlaceholder')" style="width:100%" />
</el-form-item>
<el-form-item :label="$t('tunnelEditor.autoStart')"><el-switch v-model="form.auto_start" /></el-form-item>
<el-form-item :label="$t('common.remark')"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="save">{{ $t('tunnelEditor.saveConfig') }}</el-button>
<el-button @click="router.push('/tunnels')">{{ $t('common.cancel') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import api from '../api'
import RouteListInput from '../components/RouteListInput.vue'
import OpenVPNOIDCGuide from '../components/OpenVPNOIDCGuide.vue'
import { CUSTOM_FIELDS, PROTOCOL_META, protocolColor, protocolDisplay, optionValue, optionLabel } from '../protocols'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const loading = ref(true)
const saving = ref(false)
const protocol = ref('')
const originalAuthMode = ref('')
const form = ref(null)
const advancedOpen = ref([])
const routeListRef = ref(null)
const fields = computed(() => PROTOCOL_META[protocol.value]?.fields || CUSTOM_FIELDS)
const visibleFields = computed(() => fields.value.filter(field => {
if (!field.showIf) return true
return Object.entries(field.showIf).every(([key, value]) => Array.isArray(value)
? value.includes(form.value.config[key])
: form.value.config[key] === value)
}))
const isOpenVPN = computed(() => protocol.value === 'openvpn')
const openVPNCoreKeys = new Set(['public_endpoint', 'port', 'proto', 'auth_mode', 'mfa_mode'])
const openVPNTrafficKeys = new Set(['redirect_gateway', 'push_routes', 'enable_nat'])
const trafficMode = computed({
get() {
const cfg = form.value?.config || {}
return cfg.redirect_gateway === false ? 'static' : 'gateway'
},
set(mode) {
if (!form.value) return
if (mode === 'gateway') {
form.value.config.redirect_gateway = true
form.value.config.enable_nat = true
form.value.config.push_routes = ''
} else if (mode === 'static') {
form.value.config.redirect_gateway = false
form.value.config.enable_nat = false
}
},
})
const coreFields = computed(() => isOpenVPN.value
? visibleFields.value.filter(field => openVPNCoreKeys.has(field.key))
: visibleFields.value.filter(field => field.group !== 'advanced' && field.group !== 'crypto'))
const advancedFields = computed(() => {
if (!isOpenVPN.value) return visibleFields.value.filter(field => field.group === 'advanced' || field.group === 'crypto')
return visibleFields.value.filter(field => !openVPNCoreKeys.has(field.key) && !openVPNTrafficKeys.has(field.key))
})
function parseConfig(raw) {
try { return JSON.parse(raw || '{}') } catch { return {} }
}
async function load() {
try {
const { data } = await api.get(`/tunnels/${route.params.id}`)
const tunnel = data.tunnel
protocol.value = tunnel.protocol
const config = parseConfig(tunnel.config)
originalAuthMode.value = config.auth_mode || ''
for (const field of (PROTOCOL_META[protocol.value]?.fields || CUSTOM_FIELDS)) {
if (config[field.key] === undefined && field.default !== undefined) config[field.key] = field.default
}
form.value = {
name: tunnel.name,
remark: tunnel.remark,
auto_start: tunnel.auto_start,
expire_at: tunnel.expire_at,
config,
}
} catch (error) {
ElMessage.error(error.response?.data?.error || t('tunnelEditor.loadFailed'))
router.push('/tunnels')
} finally {
loading.value = false
}
}
async function save() {
if (!form.value.name) return ElMessage.warning(t('tunnelEditor.fillName'))
if (isOpenVPN.value && form.value.config.auth_mode === 'oidc') {
for (const key of ['oidc_issuer', 'oidc_client_id', 'oidc_base_url', ...(originalAuthMode.value === 'oidc' ? [] : ['oidc_client_secret'])]) {
if (!form.value.config[key]) return ElMessage.warning(t('oidcGuide.fillRequired'))
}
if (!form.value.config.oidc_http_port) return ElMessage.warning(t('oidcGuide.portLookupFailed'))
}
trafficMode.value = trafficMode.value
if (isOpenVPN.value && routeListRef.value && !routeListRef.value.validate()) return
saving.value = true
try {
await api.put(`/tunnels/${route.params.id}`, form.value)
ElMessage.success(t('tunnelEditor.configSaved'))
router.push('/tunnels')
} catch (error) {
ElMessage.error(error.response?.data?.error || t('common.saveFailed'))
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<style scoped>
.field-tip { margin-top: 4px; color: #909399; font-size: 12px; line-height: 1.4; }
.config-section-divider { margin: 4px 0 18px; }
.advanced-options { margin: 12px 0 18px; }
</style>