2025-01-28 13:55:58 +01:00
|
|
|
<script setup>
|
|
|
|
|
|
|
|
|
|
import { ref, useTemplateRef } from 'vue';
|
2025-10-10 16:10:16 +02:00
|
|
|
import { Dialog, TextInput, InputGroup, FormGroup, Checkbox, Button } from '@cloudron/pankow';
|
|
|
|
|
import { getTextFromFile } from '../utils.js';
|
2025-01-29 16:29:21 +01:00
|
|
|
import DomainsModel from '../models/DomainsModel.js';
|
2025-04-01 14:44:37 +02:00
|
|
|
import DomainProviderForm from './DomainProviderForm.vue';
|
2025-01-29 16:29:21 +01:00
|
|
|
|
|
|
|
|
const emit = defineEmits([ 'success' ]);
|
|
|
|
|
|
2025-01-31 21:02:48 +01:00
|
|
|
const domainsModel = DomainsModel.create();
|
2025-01-28 21:25:12 +01:00
|
|
|
|
2025-01-28 13:55:58 +01:00
|
|
|
const dialog = useTemplateRef('dialog');
|
2025-10-10 16:10:16 +02:00
|
|
|
const keyFileInput = useTemplateRef('keyFileInput');
|
|
|
|
|
const certificateFileInput = useTemplateRef('certificateFileInput');
|
2025-01-28 13:55:58 +01:00
|
|
|
|
2025-01-28 21:25:12 +01:00
|
|
|
const busy = ref(false);
|
2025-01-29 16:29:21 +01:00
|
|
|
const errorMessage = ref('');
|
2025-01-29 12:15:53 +01:00
|
|
|
const editing = ref(false);
|
2025-01-28 13:55:58 +01:00
|
|
|
const domain = ref('');
|
2025-01-28 21:25:12 +01:00
|
|
|
const zoneName = ref('');
|
2025-01-28 13:55:58 +01:00
|
|
|
const provider = ref('');
|
2025-01-29 12:15:53 +01:00
|
|
|
const tlsProvider = ref('letsencrypt-prod-wildcard');
|
|
|
|
|
const showAdvanced = ref(false);
|
2025-03-02 07:27:09 +01:00
|
|
|
const customNameservers = ref(false);
|
2025-10-10 16:10:16 +02:00
|
|
|
const certificateFileName = ref('');
|
|
|
|
|
const keyFileName = ref('');
|
2025-05-02 15:34:10 +02:00
|
|
|
|
2025-05-03 09:46:53 +02:00
|
|
|
const dnsConfig = ref(DomainsModel.createEmptyConfig());
|
2025-01-29 12:15:53 +01:00
|
|
|
|
2025-10-08 15:55:43 +02:00
|
|
|
const form = useTemplateRef('form');
|
2025-01-29 12:15:53 +01:00
|
|
|
const isFormValid = ref(false);
|
|
|
|
|
function checkValidity() {
|
|
|
|
|
isFormValid.value = form.value.checkValidity();
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 16:29:21 +01:00
|
|
|
async function onSubmit() {
|
2025-01-29 12:15:53 +01:00
|
|
|
if (!form.value.reportValidity()) return;
|
|
|
|
|
|
2025-01-29 16:29:21 +01:00
|
|
|
busy.value = true;
|
|
|
|
|
errorMessage.value = '';
|
|
|
|
|
|
2025-04-01 14:44:37 +02:00
|
|
|
const config = dnsConfig.value;
|
|
|
|
|
config.customNameservers = customNameservers.value;
|
2025-01-29 16:29:21 +01:00
|
|
|
|
|
|
|
|
const tlsConfig = {
|
|
|
|
|
provider: tlsProvider.value,
|
|
|
|
|
wildcard: false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// UI uses -wildcard providers, API uses wildcard flag
|
|
|
|
|
if (tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
|
|
|
|
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
|
|
|
|
tlsConfig.wildcard = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 16:10:16 +02:00
|
|
|
let fallbackCertificate = null;
|
|
|
|
|
if (tlsConfig.provider === 'fallback') {
|
|
|
|
|
const certFile = certificateFileInput.value.files[0] || null;
|
|
|
|
|
const keyFile = keyFileInput.value.files[0] || null;
|
|
|
|
|
|
|
|
|
|
if ((!certFile && keyFile) || (certFile && !keyFile)) {
|
|
|
|
|
errorMessage.value = 'Both certificate and key file need to be provided';
|
|
|
|
|
busy.value = false;
|
|
|
|
|
return;
|
|
|
|
|
} else if (certFile && keyFile) {
|
|
|
|
|
fallbackCertificate = {
|
|
|
|
|
cert: await getTextFromFile(certFile),
|
|
|
|
|
key: await getTextFromFile(keyFile),
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
// unchanged
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 16:29:21 +01:00
|
|
|
const func = editing.value ? domainsModel.update : domainsModel.add;
|
2025-10-10 16:10:16 +02:00
|
|
|
const [error] = await func(domain.value, zoneName.value, provider.value, config, fallbackCertificate, tlsConfig);
|
2025-01-29 16:29:21 +01:00
|
|
|
if (error) {
|
|
|
|
|
errorMessage.value = error.body ? error.body.message : 'Internal error';
|
|
|
|
|
busy.value = false;
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit('success');
|
|
|
|
|
dialog.value.close();
|
2025-01-29 12:15:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-10 16:10:16 +02:00
|
|
|
function onCertificateFileChange() {
|
|
|
|
|
const file = certificateFileInput.value.files[0] || '';
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
certificateFileName.value = file ? file.name : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onKeyFileChange() {
|
|
|
|
|
const file = keyFileInput.value.files[0] || '';
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
keyFileName.value = file ? file.name : '';
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-28 13:55:58 +01:00
|
|
|
defineExpose({
|
|
|
|
|
open(d) {
|
2025-10-09 13:08:43 +02:00
|
|
|
d = d ? JSON.parse(JSON.stringify(d)) : { config: {}, tlsConfig: { provider: 'letsencrypt-prod', wildcard: true } }; // make a copy
|
2025-01-28 13:55:58 +01:00
|
|
|
|
2025-04-01 14:44:37 +02:00
|
|
|
provider.value = d.provider || '';
|
|
|
|
|
dnsConfig.value = d.config;
|
2025-09-24 20:30:08 +02:00
|
|
|
if (d.tlsConfig.wildcard) {
|
|
|
|
|
tlsProvider.value = d.tlsConfig.provider + '-wildcard';
|
|
|
|
|
} else {
|
|
|
|
|
tlsProvider.value = d.tlsConfig.provider;
|
|
|
|
|
}
|
2025-04-01 14:44:37 +02:00
|
|
|
|
2025-01-28 21:25:12 +01:00
|
|
|
busy.value = false;
|
2025-01-29 16:29:21 +01:00
|
|
|
showAdvanced.value = false;
|
|
|
|
|
errorMessage.value = '';
|
2025-01-29 12:15:53 +01:00
|
|
|
editing.value = !!d.domain;
|
2025-01-28 21:25:12 +01:00
|
|
|
domain.value = d.domain || '';
|
|
|
|
|
zoneName.value = d.zoneName || '';
|
2025-03-02 07:27:09 +01:00
|
|
|
customNameservers.value = d.config.customNameservers;
|
2025-10-10 16:10:16 +02:00
|
|
|
certificateFileName.value = d.tlsConfig?.provider === 'fallback' ? String.fromCharCode(0x25CF).repeat(8) : '';
|
|
|
|
|
keyFileName.value = d.tlsConfig?.provider === 'fallback' ? String.fromCharCode(0x25CF).repeat(8) : '';
|
2025-03-02 07:27:09 +01:00
|
|
|
|
2025-01-28 13:55:58 +01:00
|
|
|
dialog.value.open();
|
2025-04-19 16:28:36 +02:00
|
|
|
|
2025-10-08 15:55:43 +02:00
|
|
|
setTimeout(checkValidity, 100); // update state of the confirm button
|
2025-01-28 13:55:58 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<Dialog ref="dialog"
|
2025-01-29 12:15:53 +01:00
|
|
|
:title="editing ? $t('domains.domainDialog.editTitle', { domain: domain }) : $t('domains.domainDialog.addTitle')"
|
2025-01-29 16:29:21 +01:00
|
|
|
:confirm-busy="busy"
|
2025-04-19 16:28:36 +02:00
|
|
|
:confirm-active="!busy && isFormValid"
|
2025-01-29 12:15:53 +01:00
|
|
|
:confirm-label="$t('main.dialog.save')"
|
2025-10-08 16:49:05 +02:00
|
|
|
:reject-label="$t('main.dialog.cancel')"
|
|
|
|
|
:reject-active="!busy"
|
2025-01-29 12:15:53 +01:00
|
|
|
reject-style="secondary"
|
|
|
|
|
@confirm="onSubmit()"
|
2025-01-28 21:25:12 +01:00
|
|
|
>
|
2025-01-29 12:15:53 +01:00
|
|
|
<form ref="form" @submit.prevent="onSubmit()" autocomplete="off" @input="checkValidity()">
|
2025-01-29 16:29:21 +01:00
|
|
|
<fieldset :disabled="busy">
|
2025-01-28 21:25:12 +01:00
|
|
|
<input style="display: none;" type="submit" :disabled="busy"/>
|
2025-04-19 16:28:36 +02:00
|
|
|
|
2025-08-11 19:07:02 +02:00
|
|
|
<div class="error-label" v-show="errorMessage">{{ errorMessage }}</div>
|
2025-01-28 21:25:12 +01:00
|
|
|
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label for="domainInput">{{ $t('domains.domainDialog.domain') }}</label>
|
2025-01-29 16:29:21 +01:00
|
|
|
<TextInput id="domainInput" v-model="domain" placeholder="example.com" :readonly="editing ? true : undefined" required />
|
2025-01-28 21:25:12 +01:00
|
|
|
</FormGroup>
|
|
|
|
|
|
2025-10-10 16:30:36 +02:00
|
|
|
<DomainProviderForm v-model:provider="provider" v-model:dns-config="dnsConfig" v-model:tls-provider="tlsProvider" v-model:zone-name="zoneName" v-model:custom-nameservers="customNameservers" :domain="domain" :show-advanced="showAdvanced" />
|
2025-01-29 12:15:53 +01:00
|
|
|
|
|
|
|
|
<div v-show="showAdvanced">
|
|
|
|
|
<div v-if="tlsProvider === 'fallback'">
|
2025-10-10 16:10:16 +02:00
|
|
|
<label>{{ $t('domains.domainDialog.fallbackCertCustomCert') }}</label>
|
2025-01-29 12:15:53 +01:00
|
|
|
<p v-html="$t('domains.domainDialog.fallbackCertCustomCertInfo', { customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates' })"></p>
|
|
|
|
|
</div>
|
2025-01-28 21:25:12 +01:00
|
|
|
|
2025-10-10 16:10:16 +02:00
|
|
|
<div v-if="tlsProvider === 'fallback'">
|
|
|
|
|
<input type="file" ref="certificateFileInput" style="display: none" @change="onCertificateFileChange"/>
|
|
|
|
|
<input type="file" ref="keyFileInput" style="display: none" @change="onKeyFileChange"/>
|
|
|
|
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: auto 1fr; gap: 5px 10px">
|
|
|
|
|
<label>{{ $t('domains.domainDialog.fallbackCertCertificatePlaceholder') }}</label>
|
|
|
|
|
<InputGroup>
|
|
|
|
|
<TextInput v-model="certificateFileName" @click="certificateFileInput.click()" style="cursor: pointer; flex-grow: 1;" :disabled="busy" />
|
|
|
|
|
<Button tool secondary icon="fa-solid fa-upload" @click="certificateFileInput.click()"></Button>
|
|
|
|
|
</InputGroup>
|
|
|
|
|
<label>{{ $t('domains.domainDialog.fallbackCertKeyPlaceholder') }}</label>
|
|
|
|
|
<InputGroup>
|
|
|
|
|
<TextInput v-model="keyFileName" @click="keyFileInput.click()" style="cursor: pointer; flex-grow: 1;" :disabled="busy" />
|
|
|
|
|
<Button tool secondary icon="fa-solid fa-upload" @click="keyFileInput.click()"></Button>
|
|
|
|
|
</InputGroup>
|
2025-01-29 12:15:53 +01:00
|
|
|
</div>
|
2025-10-10 16:10:16 +02:00
|
|
|
</div>
|
2025-01-28 21:25:12 +01:00
|
|
|
</div>
|
2025-10-10 14:47:10 +02:00
|
|
|
|
|
|
|
|
<div v-if="!showAdvanced" style="margin-top: 15px" class="actionable" @click="showAdvanced = true">{{ $t('domains.domainDialog.advancedAction') }}</div>
|
2025-01-28 21:25:12 +01:00
|
|
|
</fieldset>
|
|
|
|
|
</form>
|
2025-01-28 13:55:58 +01:00
|
|
|
</Dialog>
|
|
|
|
|
</template>
|