blob: 3950c85248d02017615af403dee68ea336d8af59 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
@finish="handleSubmit">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label
:title="$t('label.name')"
:tooltip="apiParams.name?.description" />
</template>
<a-input
v-model:value="form.name"
:placeholder="apiParams.name?.description"
v-focus="true" />
</a-form-item>
<a-form-item name="url" ref="url">
<template #label>
<tooltip-label
:title="$t('label.url')"
:tooltip="apiParams.url?.description" />
</template>
<a-input
v-model:value="form.url"
:placeholder="apiParams.url?.description" />
</a-form-item>
<a-form-item name="provider" ref="provider">
<template #label>
<tooltip-label
:title="$t('label.provider')"
:tooltip="apiParams.provider?.description" />
</template>
<a-select
v-model:value="form.provider"
:placeholder="apiParams.provider?.description || 'Select Provider'"
:loading="fetchingProviders"
showSearch>
<a-select-option
v-for="provider in providers"
:key="provider.name"
:value="provider.name">
{{ provider.description || provider.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="port" ref="port">
<template #label>
<tooltip-label
:title="$t('label.port')"
:tooltip="apiParams.port?.description" />
</template>
<a-input-number
v-model:value="form.port"
:min="1"
:max="65535"
style="width: 100%" />
</a-form-item>
<a-form-item name="dnsapikey" ref="dnsapikey">
<template #label>
<tooltip-label
:title="$t('label.dns.dnsapikey')"
:tooltip="apiParams.dnsapikey?.description" />
</template>
<a-input-password
v-model:value="form.dnsapikey"
:placeholder="apiParams.dnsapikey?.description || 'Enter API Key'" />
</a-form-item>
<a-form-item v-if="form.provider === 'PowerDNS'" name="pdnsserverid" ref="pdnsserverid">
<template #label>
<tooltip-label
:title="$t('label.dns.pdnsserverid')"
:tooltip="'PowerDNS Server ID'" />
</template>
<a-input
v-model:value="form.pdnsserverid"
:placeholder="'Enter PowerDNS Server ID (optional if localhost)'" />
</a-form-item>
<Transition name="slide-down">
<a-form-item v-if="isAdminOrDomainAdmin() && form.ispublic" name="publicdomainsuffix" ref="publicdomainsuffix">
<template #label>
<tooltip-label
:title="$t('label.dns.publicdomainsuffix')"
:tooltip="apiParams.publicdomainsuffix?.description" />
</template>
<a-input
v-model:value="form.publicdomainsuffix"
:placeholder="apiParams.publicdomainsuffix?.description || 'Enter Public Domain Suffix e.g. example.com'" />
</a-form-item>
</Transition>
<a-form-item name="nameservers" ref="nameservers">
<template #label>
<tooltip-label
:title="$t('label.nameservers')"
:tooltip="apiParams.nameservers?.description" />
</template>
<a-select
v-model:value="form.nameservers"
mode="tags"
style="width: 100%"
:token-separators="[',', ' ']"
:placeholder="apiParams.nameservers?.description"
@change="onNameserversChange" />
</a-form-item>
<a-form-item v-if="isAdminOrDomainAdmin()" name="ispublic" ref="ispublic">
<template #label>
<tooltip-label :title="$t('label.ispublic')" :tooltip="apiParams.ispublic?.description" />
</template>
<a-switch v-model:checked="form.ispublic" />
</a-form-item>
<div class="action-button">
<a-button @click="closeAction">
{{ $t('label.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
const FQDN_REGEX = /^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}$/
export default {
name: 'AddDnsServer',
components: {
TooltipLabel
},
data () {
return {
loading: false,
apiParams: {},
form: {
name: '',
url: '',
provider: '',
port: 8081,
nameservers: [],
ispublic: false,
publicdomainsuffix: '',
pdnsserverid: ''
},
rules: {},
fetchingProviders: false,
providers: []
}
},
created () {
this.apiParams = this.$getApiParams('addDnsServer') || {}
this.rules = {
name: [{ required: true, message: this.$t('message.error.required.input') }],
url: [
{ required: true, message: this.$t('message.error.required.input') },
{ validator: this.validateUrl }
],
port: [
{ required: true, message: this.$t('message.error.required.input') },
{ validator: this.validatePort }
],
provider: [{ required: true, message: this.$t('message.error.required.input') }],
dnsapikey: [{ required: true, message: this.$t('message.error.required.input') }],
nameservers: [
{ required: true, type: 'array', min: 1, message: this.$t('message.error.required.input') },
{ validator: this.validateNameservers }
]
}
if (this.isAdminOrDomainAdmin()) {
this.rules.publicdomainsuffix = [{ validator: this.validatePublicDomainSuffix }]
}
this.fetchProviders()
},
methods: {
async handleSubmit () {
if (this.loading) return
try {
await this.$refs.formRef.validate()
} catch (error) {
if (error.errorFields && error.errorFields.length > 0) {
this.$refs.formRef.scrollToField(error.errorFields[0].name)
}
return
}
this.loading = true
try {
const params = {
name: this.form.name.trim(),
url: this.form.url?.trim().replace(/\/+$/, ''),
port: this.form.port,
provider: this.form.provider,
dnsapikey: this.form.dnsapikey?.trim(),
nameservers: this.form.nameservers || [],
ispublic: this.form.ispublic
}
if (this.form.publicdomainsuffix) {
params.publicdomainsuffix = this.form.publicdomainsuffix?.toLowerCase().trim()
}
if (this.form.pdnsserverid) {
params['details[0].pdnsServerId'] = this.form.pdnsserverid?.trim()
}
const response = await postAPI('addDnsServer', params)
const serverData = response?.adddnsserverresponse?.dnsserver || response?.adddnsserverresponse
if (!serverData?.id) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: 'Failed to get server from response',
duration: 0
})
this.loading = false
return
}
this.$notification.success({
message: this.$t('label.dns.add.server'),
description: `${this.$t('message.success.add.dns.server')} ${params.name}`
})
this.loading = false
this.$emit('refresh-data')
this.closeAction()
} catch (error) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
this.loading = false
}
},
closeAction () {
this.$emit('close-action')
},
async fetchProviders () {
this.fetchingProviders = true
try {
const response = await getAPI('listDnsProviders')
const listResponse = response?.listdnsprovidersresponse || {}
this.providers = Array.isArray(listResponse.dnsprovider) ? listResponse.dnsprovider : []
if (!this.form.provider && this.providers.length > 0) {
this.form.provider = this.providers[0].name || ''
}
} catch (error) {
console.error('Failed to fetch DNS providers', error)
this.providers = []
this.form.provider = ''
this.$message.warning('Could not load DNS providers.')
} finally {
this.fetchingProviders = false
}
},
validateUrl (rule, value) {
if (!value) return Promise.resolve()
try {
const parsed = new URL(value)
if (!['http:', 'https:'].includes(parsed.protocol)) {
return Promise.reject(new Error('URL must start with http:// or https://'))
}
if (parsed.port) {
return Promise.reject(new Error('Do not include port in URL. Use the port field instead.'))
}
} catch (e) {
return Promise.reject(new Error('Invalid URL format'))
}
return Promise.resolve()
},
validatePort (rule, value) {
if (value === undefined || value === null) {
return Promise.resolve()
}
if (value < 1 || value > 65535) {
return Promise.reject(new Error('Port must be between 1 and 65535'))
}
return Promise.resolve()
},
validateNameservers (rule, value) {
if (!value || !value.length) {
return Promise.resolve()
}
for (const ns of value) {
if (!FQDN_REGEX.test(ns)) {
return Promise.reject(new Error('Invalid nameserver'))
}
}
if (new Set(value).size !== value.length) {
return Promise.reject(new Error('Nameservers must be unique'))
}
return Promise.resolve()
},
validatePublicDomainSuffix (rule, value) {
const normalized = value?.toLowerCase().trim()
if (!normalized) {
return Promise.resolve()
}
if (!FQDN_REGEX.test(normalized)) {
return Promise.reject(new Error('Invalid domain suffix'))
}
return Promise.resolve()
},
isAdminOrDomainAdmin () {
return ['Admin', 'DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
},
onNameserversChange (value) {
this.form.nameservers = (value || [])
.map(ns => ns?.toLowerCase().trim())
.filter(Boolean)
}
}
}
</script>
<style lang="less" scoped>
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 450px;
}
}
.action-button {
text-align: right;
margin-top: 20px;
}
.action-button button {
margin-left: 8px;
}
.slide-down-enter-active,
.slide-down-leave-active {
transition: max-height 0.3s ease, opacity 0.3s ease;
overflow: hidden;
max-height: 120px;
}
.slide-down-enter-from,
.slide-down-leave-to {
max-height: 0;
opacity: 0;
}
</style>