2025-04-01 19:04:10 +02:00
|
|
|
<script setup>
|
|
|
|
|
|
2025-04-02 12:43:59 +02:00
|
|
|
import { ref, onMounted, useTemplateRef } from 'vue';
|
2025-07-22 18:03:40 +02:00
|
|
|
import { ProgressBar, Button, SingleSelect, FormGroup, TextInput, Notification } from '@cloudron/pankow';
|
2025-07-10 11:55:11 +02:00
|
|
|
import { copyToClipboard } from '@cloudron/pankow/utils';
|
2025-04-01 19:04:10 +02:00
|
|
|
import { redirectIfNeeded } from '../utils.js';
|
2025-05-03 09:51:32 +02:00
|
|
|
import DomainsModel from '../models/DomainsModel.js';
|
2025-04-02 12:43:59 +02:00
|
|
|
import ProvisionModel from '../models/ProvisionModel.js';
|
2025-04-01 19:04:10 +02:00
|
|
|
import DomainProviderForm from '../components/DomainProviderForm.vue';
|
|
|
|
|
|
2025-04-02 12:43:59 +02:00
|
|
|
const provisionModel = ProvisionModel.create();
|
|
|
|
|
|
|
|
|
|
const ipProviders = [
|
|
|
|
|
{ name: 'Disabled', value: 'noop' },
|
|
|
|
|
{ name: 'Public IP', value: 'generic' },
|
|
|
|
|
{ name: 'Static IP Address', value: 'fixed' },
|
|
|
|
|
{ name: 'Network Interface', value: 'network-interface' }
|
|
|
|
|
];
|
2025-04-01 19:04:10 +02:00
|
|
|
|
|
|
|
|
const formError = ref({});
|
|
|
|
|
const busy = ref(false);
|
|
|
|
|
const ready = ref(false);
|
2025-05-28 14:51:12 +02:00
|
|
|
const waitingForDnsSetup = ref(false);
|
2025-04-01 19:04:10 +02:00
|
|
|
const progressMessage = ref('');
|
|
|
|
|
const taskMinutesActive = ref(0);
|
|
|
|
|
const domain = ref('');
|
2025-04-02 12:43:59 +02:00
|
|
|
const instanceId = ref('');
|
|
|
|
|
const setupToken = ref('');
|
2025-04-01 19:04:10 +02:00
|
|
|
const zoneName = ref('');
|
|
|
|
|
const provider = ref('');
|
2025-05-03 09:51:32 +02:00
|
|
|
const dnsConfig = ref(DomainsModel.createEmptyConfig());
|
2025-04-02 12:43:59 +02:00
|
|
|
const tlsProvider = ref('letsencrypt-prod-wildcard');
|
|
|
|
|
const showAdvanced = ref(false);
|
|
|
|
|
const ipv4Provider = ref('generic');
|
|
|
|
|
const ipv4Address = ref('');
|
|
|
|
|
const ipv4Interface = ref('');
|
|
|
|
|
const ipv6Provider = ref('generic');
|
|
|
|
|
const ipv6Address = ref('');
|
|
|
|
|
const ipv6Interface = ref('');
|
|
|
|
|
|
|
|
|
|
const form = useTemplateRef('form');
|
|
|
|
|
const isFormValid = ref(false);
|
|
|
|
|
function checkValidity() {
|
|
|
|
|
if (!provider.value) return false;
|
|
|
|
|
isFormValid.value = form.value.checkValidity();
|
|
|
|
|
}
|
2025-04-01 19:04:10 +02:00
|
|
|
|
2025-04-02 12:43:59 +02:00
|
|
|
async function waitForDnsSetup () {
|
|
|
|
|
waitingForDnsSetup.value = true;
|
|
|
|
|
formError.value = {};
|
|
|
|
|
|
|
|
|
|
const [error, result] = await provisionModel.status();
|
|
|
|
|
if (error) {
|
|
|
|
|
setTimeout(waitForDnsSetup, 5000);
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!result.setup.active) {
|
|
|
|
|
if (!result.adminFqdn || result.setup.errorMessage) { // setup reset or errored. start over
|
|
|
|
|
formError.value.dnsWait = result.setup.errorMessage;
|
|
|
|
|
waitingForDnsSetup.value = false;
|
|
|
|
|
} else { // proceed to activation
|
|
|
|
|
window.location.href = 'https://' + result.adminFqdn + '/activation.html' + (window.location.search);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
progressMessage.value = result.setup.message;
|
|
|
|
|
taskMinutesActive.value = (new Date() - new Date(result.setup.startTime)) / 60000;
|
|
|
|
|
|
|
|
|
|
setTimeout(waitForDnsSetup, 5000);
|
|
|
|
|
}
|
2025-04-01 19:04:10 +02:00
|
|
|
|
|
|
|
|
async function onSubmit() {
|
2025-04-02 12:43:59 +02:00
|
|
|
if (!isFormValid.value) return;
|
2025-04-01 19:04:10 +02:00
|
|
|
|
|
|
|
|
busy.value = true;
|
|
|
|
|
formError.value = {};
|
2025-04-02 12:43:59 +02:00
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
domainConfig: {
|
|
|
|
|
domain: domain.value,
|
|
|
|
|
zoneName: zoneName.value,
|
|
|
|
|
provider: provider.value,
|
2025-05-03 09:51:32 +02:00
|
|
|
config: DomainsModel.filterConfigForProvider(provider.value, dnsConfig.value),
|
2025-04-02 12:43:59 +02:00
|
|
|
tlsConfig: {
|
2025-04-02 15:31:43 +02:00
|
|
|
provider: tlsProvider.value.replace('-wildcard', ''),
|
|
|
|
|
wildcard: tlsProvider.value.indexOf('-wildcard') !== -1 ? true : false,
|
2025-04-02 12:43:59 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ipv4Config: {
|
|
|
|
|
provider: ipv4Provider.value,
|
|
|
|
|
ip: ipv4Provider.value === 'fixed' ? ipv4Address.value : '',
|
|
|
|
|
ifname: ipv4Provider.value === 'network-interface' ? ipv4Interface.value : '',
|
|
|
|
|
},
|
|
|
|
|
ipv6Config: {
|
|
|
|
|
provider: ipv6Provider.value,
|
|
|
|
|
ip: ipv6Provider.value === 'fixed' ? ipv6Address.value : '',
|
|
|
|
|
ifname: ipv6Provider.value === 'network-interface' ? ipv6Interface.value : '',
|
|
|
|
|
},
|
|
|
|
|
providerToken: instanceId.value,
|
|
|
|
|
setupToken: setupToken.value,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [error] = await provisionModel.setup(data);
|
2025-09-07 12:46:16 +02:00
|
|
|
busy.value = false; // so we can come back to this view if dns setup errors later
|
2025-04-02 12:43:59 +02:00
|
|
|
if (error) {
|
|
|
|
|
if (error.status === 422) {
|
|
|
|
|
if (provider.value === 'ami') {
|
2025-04-02 15:45:57 +02:00
|
|
|
formError.value.ami = error.body.message;
|
2025-04-02 12:43:59 +02:00
|
|
|
} else {
|
2025-04-02 15:45:57 +02:00
|
|
|
formError.value.setup = error.body.message;
|
2025-04-02 12:43:59 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2025-04-02 15:45:57 +02:00
|
|
|
formError.value.generic = error.body ? error.body.message : 'Internal error';
|
2025-04-02 12:43:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
waitForDnsSetup();
|
2025-04-01 19:04:10 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-27 13:55:36 +02:00
|
|
|
function onCopyToClipboard(value) {
|
|
|
|
|
copyToClipboard(value);
|
|
|
|
|
window.pankow.notify('copied to clipboard');
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 19:04:10 +02:00
|
|
|
onMounted(async () => {
|
2025-04-02 12:43:59 +02:00
|
|
|
const search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
|
|
|
|
|
2025-09-07 13:01:51 +02:00
|
|
|
let [error, result] = await provisionModel.status();
|
2025-04-01 19:04:10 +02:00
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
if (redirectIfNeeded(result, 'setup')) return; // redirected to some other view...
|
|
|
|
|
|
2025-09-07 13:01:51 +02:00
|
|
|
if (result.setup.active) return waitForDnsSetup();
|
|
|
|
|
formError.value.generic = result.setup.errorMessage; // show any previous error
|
|
|
|
|
|
|
|
|
|
if (result.provider === 'digitalocean' || result.provider === 'digitalocean-mp') {
|
|
|
|
|
provider.value = 'digitalocean';
|
|
|
|
|
} else if (result.provider === 'linode' || result.provider === 'linode-oneclick' || result.provider === 'linode-stackscript') {
|
|
|
|
|
provider.value = 'linode';
|
|
|
|
|
} else if (result.provider === 'vultr' || result.provider === 'vultr-mp') {
|
|
|
|
|
provider.value = 'vultr';
|
|
|
|
|
} else if (result.provider === 'gce') {
|
|
|
|
|
provider.value = 'gcdns';
|
|
|
|
|
} else if (result.provider === 'ami') {
|
|
|
|
|
// aws marketplace made a policy change that they one cannot provide route53 IAM credentials
|
|
|
|
|
provider.value = 'wildcard';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[error, result] = await provisionModel.detectIp();
|
|
|
|
|
if (error) return console.error(error); // detectIp is not supposed to error
|
|
|
|
|
|
|
|
|
|
ipv4Provider.value = result.ipv4 ? 'generic' : 'noop';
|
|
|
|
|
ipv6Provider.value = result.ipv6 ? 'generic' : 'noop';
|
|
|
|
|
|
2025-04-02 12:43:59 +02:00
|
|
|
instanceId.value = search.instanceId;
|
|
|
|
|
setupToken.value = search.setupToken;
|
|
|
|
|
|
2025-04-01 19:04:10 +02:00
|
|
|
ready.value = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="container" v-if="ready">
|
2025-07-22 18:03:40 +02:00
|
|
|
<Notification />
|
2025-05-27 13:55:36 +02:00
|
|
|
|
2025-08-17 17:22:50 +02:00
|
|
|
<Transition name="fade-scale" mode="out-in">
|
|
|
|
|
<div class="view" v-if="waitingForDnsSetup" style="max-width: unset; height: 100%;">
|
|
|
|
|
<div style="display: flex; flex-direction: column; height: 100%; align-items: center;">
|
|
|
|
|
<div class="gradient" v-if="waitingForDnsSetup"></div>
|
2025-07-22 18:03:40 +02:00
|
|
|
<h3>Please wait while Cloudron is setting up the dashboard</h3>
|
2025-08-17 17:22:50 +02:00
|
|
|
<h4 style="margin-top: 0">{{ progressMessage }}</h4>
|
|
|
|
|
<small>You can follow the logs on the server at <code @click="onCopyToClipboard('/home/yellowtent/platformdata/logs/box.log')">/home/yellowtent/platformdata/logs/box.log</code></small>
|
2025-04-02 12:43:59 +02:00
|
|
|
</div>
|
2025-04-01 19:04:10 +02:00
|
|
|
</div>
|
|
|
|
|
|
2025-07-22 18:03:40 +02:00
|
|
|
<div class="view" v-else style="max-width: 500px">
|
2025-09-07 12:25:39 +02:00
|
|
|
<h1 style="text-align: center">Domain Setup</h1>
|
2025-04-02 12:43:59 +02:00
|
|
|
|
|
|
|
|
<div class="text-danger" v-if="formError.dnsWait">{{ formError.dnsWait }}</div>
|
|
|
|
|
<div class="text-danger" v-if="formError.setup">{{ formError.setup }}</div>
|
|
|
|
|
<div class="text-danger" v-if="formError.generic">{{ formError.generic }}</div>
|
2025-04-01 19:04:10 +02:00
|
|
|
|
2025-04-02 12:43:59 +02:00
|
|
|
<form ref="form" @submit.prevent="onSubmit()" @input="checkValidity()">
|
2025-04-01 19:04:10 +02:00
|
|
|
<fieldset :disabled="busy">
|
2025-05-26 13:39:36 +02:00
|
|
|
<input type="submit" style="display: none" :disabled="busy || !isFormValid"/>
|
|
|
|
|
|
2025-04-01 19:04:10 +02:00
|
|
|
<FormGroup>
|
|
|
|
|
<label for="domainInput">Domain <sup><a href="https://docs.cloudron.io/installation/#domain-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
|
|
|
|
<TextInput id="domainInput" v-model="domain" placeholder="example.com" required />
|
|
|
|
|
<div class="text-danger" v-show="domain.indexOf('my.') === 0 && domain.length > 3">Are you sure about this domain? The dashboard will be at <b>my.{{ domain }}</b></div>
|
2025-05-12 15:02:28 +02:00
|
|
|
<div style="padding-top: 6px;">Apps will be installed on subdomains of this domain. The dashboard will be available on the <b>my{{ domain ? `.${domain}` : '' }}</b> subdomain. You can add more domains later.</div>
|
2025-04-01 19:04:10 +02:00
|
|
|
</FormGroup>
|
|
|
|
|
|
2025-05-26 14:35:54 +02:00
|
|
|
<DomainProviderForm :disabled="busy" v-model:provider="provider" v-model:dns-config="dnsConfig" v-model:tls-provider="tlsProvider" :domain="domain" :show-advanced="showAdvanced" />
|
2025-04-01 19:04:10 +02:00
|
|
|
|
2025-04-02 12:43:59 +02:00
|
|
|
<div v-show="showAdvanced">
|
2025-04-01 19:04:10 +02:00
|
|
|
<FormGroup>
|
|
|
|
|
<label for="zoneNameInput">DNS Zone Name (Optional) <sup><a href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
|
|
|
|
<TextInput id="zoneNameInput" v-model="zoneName" placeholder="Defaults to TLD" />
|
|
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<!-- IPv4 provider -->
|
|
|
|
|
<FormGroup>
|
2025-05-06 11:45:49 +02:00
|
|
|
<label class="control-label">IPv4 Configuration <sup><a href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
2025-04-02 12:43:59 +02:00
|
|
|
<SingleSelect v-model="ipv4Provider" :options="ipProviders" option-key="value" option-label="name" />
|
2025-04-01 19:04:10 +02:00
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<!-- IPv4 Fixed -->
|
2025-04-02 12:43:59 +02:00
|
|
|
<FormGroup v-if="ipv4Provider === 'fixed'">
|
|
|
|
|
<label for="ipv4AddressInput">IPv4 Address</label>
|
|
|
|
|
<TextInput id="ipv4AddressInput" v-model="ipv4Address" required />
|
2025-04-01 19:04:10 +02:00
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<!-- IPv4 Network Interface -->
|
2025-04-02 12:43:59 +02:00
|
|
|
<FormGroup v-if="ipv4Provider === 'network-interface'">
|
|
|
|
|
<label for="ipv4InterfaceInput">IPv4 Interface Name</label>
|
|
|
|
|
<TextInput id="ipv4InterfaceInput" v-model="ipv4Interface" required />
|
2025-04-01 19:04:10 +02:00
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<!-- IPv6 provider -->
|
|
|
|
|
<FormGroup>
|
2025-05-06 11:45:49 +02:00
|
|
|
<label class="control-label">IPv6 Configuration <sup><a href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
2025-04-02 12:43:59 +02:00
|
|
|
<SingleSelect v-model="ipv6Provider" :options="ipProviders" option-key="value" option-label="name" />
|
2025-04-01 19:04:10 +02:00
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<!-- IPv6 Fixed -->
|
2025-04-02 12:43:59 +02:00
|
|
|
<FormGroup v-if="ipv6Provider === 'fixed'">
|
|
|
|
|
<label for="ipv6AddressInput">IPv6 Address</label>
|
|
|
|
|
<TextInput id="ipv6AddressInput" v-model="ipv6Address" required />
|
2025-04-01 19:04:10 +02:00
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<!-- IPv6 Network Interface -->
|
2025-04-02 12:43:59 +02:00
|
|
|
<FormGroup v-if="ipv6Provider === 'network-interface'">
|
|
|
|
|
<label for="ipv6InterfaceInpt">IPv6 Interface Name</label>
|
|
|
|
|
<TextInput id="ipv6InterfaceInpt" v-model="ipv6Interface" required />
|
2025-04-01 19:04:10 +02:00
|
|
|
</FormGroup>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-07 12:25:39 +02:00
|
|
|
<div style="margin-top: 1.5em" class="actionable" @click="showAdvanced = false" v-if="showAdvanced">Hide Advanced settings</div>
|
|
|
|
|
<div style="margin-top: 1.5em" class="actionable" @click="showAdvanced = true" v-else>Advanced settings...</div>
|
2025-04-01 19:04:10 +02:00
|
|
|
</fieldset>
|
|
|
|
|
</form>
|
|
|
|
|
|
2025-09-09 15:55:01 +02:00
|
|
|
<div class="actions">
|
2025-07-22 18:03:40 +02:00
|
|
|
<Button @click="onSubmit()" :disabled="busy || !isFormValid" :loading="busy">Next</Button>
|
2025-09-09 15:55:01 +02:00
|
|
|
<a class="restore" href="/restore.html">Looking to restore?</a>
|
2025-07-22 18:03:40 +02:00
|
|
|
</div>
|
2025-04-01 19:04:10 +02:00
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
2025-09-09 15:55:01 +02:00
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
|
|
.actions {
|
|
|
|
|
margin-top: 1.5em;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.actions .restore {
|
|
|
|
|
margin-top: 1em;
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|