'use strict'; /* global async */ /* global angular */ /* global $, TASK_TYPES */ angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) { Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); }); $scope.config = Client.getConfig(); $scope.domains = []; $scope.ready = false; $scope.translationLinks = { linodeDocsLink: 'https://docs.cloudron.io/domains/#linode-dns', customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates' }; $scope.openSubscriptionSetup = function () { Client.openSubscriptionSetup($scope.$parent.subscription); }; // currently, validation of wildcard with various provider is done server side $scope.tlsProvider = [ { name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' }, { name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' }, { name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' }, { name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' }, { name: 'Custom Wildcard Certificate', value: 'fallback' }, ]; // keep in sync with setupdns.js $scope.dnsProvider = [ { name: 'AWS Route53', value: 'route53' }, { name: 'Cloudflare', value: 'cloudflare' }, { name: 'DigitalOcean', value: 'digitalocean' }, { name: 'Gandi LiveDNS', value: 'gandi' }, { name: 'GoDaddy', value: 'godaddy' }, { name: 'Google Cloud DNS', value: 'gcdns' }, { name: 'Hetzner', value: 'hetzner' }, { name: 'Linode', value: 'linode' }, { name: 'Name.com', value: 'namecom' }, { name: 'Namecheap', value: 'namecheap' }, { name: 'Netcup', value: 'netcup' }, { name: 'Vultr', value: 'vultr' }, { name: 'Wildcard', value: 'wildcard' }, { name: 'Manual (not recommended)', value: 'manual' }, { name: 'No-op (only for development)', value: 'noop' } ]; $scope.prettyProviderName = function (domain) { switch (domain.provider) { case 'route53': return 'AWS Route53'; case 'cloudflare': return 'Cloudflare'; case 'digitalocean': return 'DigitalOcean'; case 'gandi': return 'Gandi LiveDNS'; case 'hetzner': return 'Hetzner DNS'; case 'linode': return 'Linode'; case 'namecom': return 'Name.com'; case 'namecheap': return 'Namecheap'; case 'netcup': return 'Netcup'; case 'gcdns': return 'Google Cloud'; case 'godaddy': return 'GoDaddy'; case 'vultr': return 'Vultr'; case 'manual': return 'Manual'; case 'wildcard': return 'Wildcard'; case 'noop': return 'No-op'; default: return 'Unknown'; } }; $scope.needsPort80 = function (dnsProvider, tlsProvider) { return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') && (tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging')); }; function readFileLocally(obj, file, fileName) { return function (event) { $scope.$apply(function () { obj[file] = null; obj[fileName] = event.target.files[0].name; var reader = new FileReader(); reader.onload = function (result) { if (!result.target || !result.target.result) return console.error('Unable to read local file'); obj[file] = result.target.result; }; reader.readAsText(event.target.files[0]); }); }; } function refreshDomains(callback) { var domains = [ ]; Client.getDomains(function (error, results) { if (error) return console.error(error); async.eachSeries(results, function (result, iteratorDone) { Client.getDomain(result.domain, function (error, domain) { if (error) return iteratorDone(error); domains.push(domain); iteratorDone(); }); }, function (error) { angular.copy(domains, $scope.domains); $scope.changeDashboard.selectedDomain = $scope.changeDashboard.adminDomain = $scope.domains.find(function (d) { return d.domain === $scope.config.adminDomain; }); if (error) console.error(error); if (callback) callback(error); }); }); } $scope.domainAdd = { show: function () { $scope.domainConfigure.show(); } }; $scope.domainWellKnown = { busy: false, error: null, domain: null, mastodonHostname: '', matrixHostname: '', jitsiHostname: '', reset: function () { $scope.domainWellKnown.busy = false; $scope.domainWellKnown.error = null; $scope.domainWellKnown.domain = null; $scope.domainWellKnown.matrixHostname = ''; $scope.domainWellKnown.mastodonHostname = ''; $scope.domainWellKnown.jitsiHostname = ''; }, show: function (domain) { $scope.domainWellKnown.reset(); $scope.domainWellKnown.domain = domain; try { if (domain.wellKnown && domain.wellKnown['matrix/server']) { $scope.domainWellKnown.matrixHostname = JSON.parse(domain.wellKnown['matrix/server'])['m.server']; } if (domain.wellKnown && domain.wellKnown['host-meta']) { $scope.domainWellKnown.mastodonHostname = domain.wellKnown['host-meta'].match(new RegExp('template="https://(.*?)/'))[1]; } if (domain.wellKnown && domain.wellKnown['matrix/client']) { let parsed = JSON.parse(domain.wellKnown['matrix/client']); if (parsed['im.vector.riot.jitsi'] && parsed['im.vector.riot.jitsi']['preferredDomain']) { $scope.domainWellKnown.jitsiHostname = parsed['im.vector.riot.jitsi']['preferredDomain']; } } } catch (e) { console.error(e); } $('#domainWellKnownModal').modal('show'); }, submit: function () { $scope.domainWellKnown.busy = true; $scope.domainWellKnown.error = null; var wellKnown = {}; if ($scope.domainWellKnown.matrixHostname) { wellKnown['matrix/server'] = JSON.stringify({ 'm.server': $scope.domainWellKnown.matrixHostname }); // https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client wellKnown['matrix/client'] = JSON.stringify({ 'm.homeserver': { 'base_url': 'https://' + $scope.domainWellKnown.matrixHostname }, 'im.vector.riot.jitsi': { 'preferredDomain': $scope.domainWellKnown.jitsiHostname } }); } else if ($scope.domainWellKnown.jitsiHostname) { // only if matrixHostname is not set wellKnown['matrix/client'] = JSON.stringify({ 'im.vector.riot.jitsi': { 'preferredDomain': $scope.domainWellKnown.jitsiHostname } }); } if ($scope.domainWellKnown.mastodonHostname) { wellKnown['host-meta'] = '\n' + '\n' + '\n' + ''; } Client.updateDomainWellKnown($scope.domainWellKnown.domain.domain, wellKnown, function (error) { $scope.domainWellKnown.busy = false; if (error) { $scope.domainWellKnown.error = error.message; return; } $('#domainWellKnownModal').modal('hide'); $scope.domainWellKnown.reset(); refreshDomains(); }); } }; // We reused configure also for adding domains to avoid much code duplication $scope.domainConfigure = { adding: false, error: null, busy: false, domain: null, advancedVisible: false, // form model newDomain: '', accessKeyId: '', secretAccessKey: '', gcdnsKey: { keyFileName: '', content: '' }, digitalOceanToken: '', gandiApiKey: '', godaddyApiKey: '', godaddyApiSecret: '', cloudflareToken: '', cloudflareEmail: '', cloudflareTokenType: 'GlobalApiKey', linodeToken: '', hetznerToken: '', vultrToken: '', nameComToken: '', nameComUsername: '', namecheapUsername: '', namecheapApiKey: '', netcupCustomerNumber: '', netcupApiKey: '', netcupApiPassword: '', provider: 'route53', zoneName: '', tlsConfig: { provider: 'letsencrypt-prod-wildcard' }, fallbackCert: { certificateFile: null, certificateFileName: '', keyFile: null, keyFileName: '' }, setDefaultTlsProvider: function () { var dnsProvider = $scope.domainConfigure.provider; // wildcard LE won't work without automated DNS if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') { $scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod'; } else { $scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod-wildcard'; } }, show: function (domain) { $scope.domainConfigure.reset(); if (domain) { $scope.domainConfigure.domain = domain; $scope.domainConfigure.accessKeyId = domain.config.accessKeyId; $scope.domainConfigure.secretAccessKey = domain.config.secretAccessKey; $scope.domainConfigure.gcdnsKey.keyFileName = ''; $scope.domainConfigure.gcdnsKey.content = ''; if (domain.provider === 'gcdns') { $scope.domainConfigure.gcdnsKey.keyFileName = domain.config.credentials && domain.config.credentials.client_email; $scope.domainConfigure.gcdnsKey.content = JSON.stringify({ project_id: domain.config.projectId, client_email: domain.config.credentials.client_email, private_key: domain.config.credentials.private_key }); } $scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : ''; $scope.domainConfigure.linodeToken = domain.provider === 'linode' ? domain.config.token : ''; $scope.domainConfigure.hetznerToken = domain.provider === 'hetzner' ? domain.config.token : ''; $scope.domainConfigure.vultrToken = domain.provider === 'vultr' ? domain.config.token : ''; $scope.domainConfigure.gandiApiKey = domain.provider === 'gandi' ? domain.config.token : ''; $scope.domainConfigure.cloudflareToken = domain.provider === 'cloudflare' ? domain.config.token : ''; $scope.domainConfigure.cloudflareEmail = domain.provider === 'cloudflare' ? domain.config.email : ''; $scope.domainConfigure.cloudflareTokenType = domain.provider === 'cloudflare' ? domain.config.tokenType : 'GlobalApiKey'; $scope.domainConfigure.godaddyApiKey = domain.provider === 'godaddy' ? domain.config.apiKey : ''; $scope.domainConfigure.godaddyApiSecret = domain.provider === 'godaddy' ? domain.config.apiSecret : ''; $scope.domainConfigure.nameComToken = domain.provider === 'namecom' ? domain.config.token : ''; $scope.domainConfigure.nameComUsername = domain.provider === 'namecom' ? domain.config.username : ''; $scope.domainConfigure.namecheapApiKey = domain.provider === 'namecheap' ? domain.config.token : ''; $scope.domainConfigure.namecheapUsername = domain.provider === 'namecheap' ? domain.config.username : ''; $scope.domainConfigure.netcupCustomerNumber = domain.provider === 'netcup' ? domain.config.customerNumber : ''; $scope.domainConfigure.netcupApiKey = domain.provider === 'netcup' ? domain.config.apiKey : ''; $scope.domainConfigure.netcupApiPassword = domain.provider === 'netcup' ? domain.config.apiPassword : ''; $scope.domainConfigure.provider = domain.provider; $scope.domainConfigure.tlsConfig.provider = domain.tlsConfig.provider; if (domain.tlsConfig.provider.indexOf('letsencrypt') === 0) { if (domain.tlsConfig.wildcard) $scope.domainConfigure.tlsConfig.provider += '-wildcard'; } $scope.domainConfigure.zoneName = domain.zoneName; } else { $scope.domainConfigure.adding = true; } $('#domainConfigureModal').modal('show'); }, submit: function () { $scope.domainConfigure.busy = true; $scope.domainConfigure.error = null; var provider = $scope.domainConfigure.provider; var data = {}; if (provider === 'route53') { data.accessKeyId = $scope.domainConfigure.accessKeyId; data.secretAccessKey = $scope.domainConfigure.secretAccessKey; } else if (provider === 'gcdns') { try { var serviceAccountKey = JSON.parse($scope.domainConfigure.gcdnsKey.content); data.projectId = serviceAccountKey.project_id; data.credentials = { client_email: serviceAccountKey.client_email, private_key: serviceAccountKey.private_key }; if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) { throw new Error('One or more fields are missing in the JSON'); } } catch (e) { $scope.domainConfigure.error = 'Cannot parse Google Service Account Key: ' + e.message; $scope.domainConfigure.busy = false; return; } } else if (provider === 'digitalocean') { data.token = $scope.domainConfigure.digitalOceanToken; } else if (provider === 'linode') { data.token = $scope.domainConfigure.linodeToken; } else if (provider === 'hetzner') { data.token = $scope.domainConfigure.hetznerToken; } else if (provider === 'vultr') { data.token = $scope.domainConfigure.vultrToken; } else if (provider === 'gandi') { data.token = $scope.domainConfigure.gandiApiKey; } else if (provider === 'godaddy') { data.apiKey = $scope.domainConfigure.godaddyApiKey; data.apiSecret = $scope.domainConfigure.godaddyApiSecret; } else if (provider === 'cloudflare') { data.token = $scope.domainConfigure.cloudflareToken; data.email = $scope.domainConfigure.cloudflareEmail; data.tokenType = $scope.domainConfigure.cloudflareTokenType; } else if (provider === 'namecom') { data.token = $scope.domainConfigure.nameComToken; data.username = $scope.domainConfigure.nameComUsername; } else if (provider === 'namecheap') { data.token = $scope.domainConfigure.namecheapApiKey; data.username = $scope.domainConfigure.namecheapUsername; } else if (provider === 'netcup') { data.customerNumber = $scope.domainConfigure.netcupCustomerNumber; data.apiKey = $scope.domainConfigure.netcupApiKey; data.apiPassword = $scope.domainConfigure.netcupApiPassword; } var fallbackCertificate = null; if ($scope.domainConfigure.fallbackCert.certificateFile && $scope.domainConfigure.fallbackCert.keyFile) { fallbackCertificate = { cert: $scope.domainConfigure.fallbackCert.certificateFile, key: $scope.domainConfigure.fallbackCert.keyFile }; } var tlsConfig = { provider: $scope.domainConfigure.tlsConfig.provider, wildcard: false }; if ($scope.domainConfigure.tlsConfig.provider.indexOf('-wildcard') !== -1) { tlsConfig.provider = tlsConfig.provider.replace('-wildcard', ''); tlsConfig.wildcard = true; } // choose the right api, since we reuse this for adding and configuring domains var func; if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig); else func = Client.updateDomainConfig.bind(Client, $scope.domainConfigure.domain.domain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig); func(function (error) { $scope.domainConfigure.busy = false; if (error) { $scope.domainConfigure.error = error.message; return; } $('#domainConfigureModal').modal('hide'); $scope.domainConfigure.reset(); refreshDomains(); }); }, reset: function () { $scope.domainConfigure.adding = false; $scope.domainConfigure.advancedVisible = false; $scope.domainConfigure.newDomain = ''; $scope.domainConfigure.busy = false; $scope.domainConfigure.error = null; $scope.domainConfigure.provider = ''; $scope.domainConfigure.accessKeyId = ''; $scope.domainConfigure.secretAccessKey = ''; $scope.domainConfigure.gcdnsKey.keyFileName = ''; $scope.domainConfigure.gcdnsKey.content = ''; $scope.domainConfigure.digitalOceanToken = ''; $scope.domainConfigure.gandiApiKey = ''; $scope.domainConfigure.godaddyApiKey = ''; $scope.domainConfigure.godaddyApiSecret = ''; $scope.domainConfigure.cloudflareToken = ''; $scope.domainConfigure.cloudflareEmail = ''; $scope.domainConfigure.cloudflareTokenType = 'GlobalApiKey'; $scope.domainConfigure.nameComToken = ''; $scope.domainConfigure.nameComUsername = ''; $scope.domainConfigure.namecheapApiKey = ''; $scope.domainConfigure.namecheapUsername = ''; $scope.domainConfigure.netcupCustomerNumber = ''; $scope.domainConfigure.netcupApiKey = ''; $scope.domainConfigure.netcupApiPassword = ''; $scope.domainConfigure.vultrToken = ''; $scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod'; $scope.domainConfigure.zoneName = ''; $scope.domainConfigureForm.$setPristine(); $scope.domainConfigureForm.$setUntouched(); } }; $scope.renewCerts = { busy: false, percent: 0, message: '', errorMessage: '', taskId: '', checkStatus: function () { Client.getLatestTaskByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, task) { if (error) return console.error(error); if (!task) return; $scope.renewCerts.taskId = task.id; $scope.renewCerts.updateStatus(); }); }, updateStatus: function () { Client.getTask($scope.renewCerts.taskId, function (error, data) { if (error) return window.setTimeout($scope.renewCerts.updateStatus, 5000); if (!data.active) { $scope.renewCerts.busy = false; $scope.renewCerts.message = ''; $scope.renewCerts.percent = 100; // indicates that 'result' is valid $scope.renewCerts.errorMessage = data.success ? '' : data.error.message; return; } $scope.renewCerts.busy = true; $scope.renewCerts.percent = data.percent; $scope.renewCerts.message = data.message; window.setTimeout($scope.renewCerts.updateStatus, 500); }); }, renew: function () { $scope.renewCerts.busy = true; $scope.renewCerts.percent = 0; $scope.renewCerts.message = ''; $scope.renewCerts.errorMessage = ''; // always rebuild the nginx configs when triggered via the UI. we assume user is clicking this because something is wrong Client.renewCerts({ rebuild: true }, function (error, taskId) { if (error) { console.error(error); $scope.renewCerts.errorMessage = error.message; $scope.renewCerts.busy = false; } else { $scope.renewCerts.taskId = taskId; $scope.renewCerts.updateStatus(); } }); } }; $scope.syncDns = { busy: false, percent: 0, message: '', errorMessage: '', taskId: '', checkStatus: function () { Client.getLatestTaskByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, task) { if (error) return console.error(error); if (!task) return; $scope.syncDns.taskId = task.id; $scope.syncDns.updateStatus(); }); }, updateStatus: function () { Client.getTask($scope.syncDns.taskId, function (error, data) { if (error) return window.setTimeout($scope.syncDns.updateStatus, 5000); if (!data.active) { $scope.syncDns.busy = false; $scope.syncDns.message = ''; $scope.syncDns.percent = 100; // indicates that 'result' is valid $scope.syncDns.errorMessage = data.success ? '' : data.error.message; return; } $scope.syncDns.busy = true; $scope.syncDns.percent = data.percent; $scope.syncDns.message = data.message; window.setTimeout($scope.syncDns.updateStatus, 500); }); }, sync: function () { $scope.syncDns.busy = true; $scope.syncDns.percent = 0; $scope.syncDns.message = ''; $scope.syncDns.errorMessage = ''; Client.setDnsRecords({}, function (error, taskId) { if (error) { console.error(error); $scope.syncDns.errorMessage = error.message; $scope.syncDns.busy = false; } else { $scope.syncDns.taskId = taskId; $scope.syncDns.updateStatus(); } }); } }; $scope.domainRemove = { busy: false, error: null, domain: null, show: function (domain) { $scope.domainRemove.reset(); $scope.domainRemove.domain = domain; $('#domainRemoveModal').modal('show'); }, submit: function () { $scope.domainRemove.busy = true; $scope.domainRemove.error = null; Client.removeDomain($scope.domainRemove.domain.domain, function (error) { if (error && (error.statusCode === 403 || error.statusCode === 409)) { $scope.domainRemove.error = error.message; } else if (error) { Client.error(error); } else { $('#domainRemoveModal').modal('hide'); $scope.domainRemove.reset(); refreshDomains(); } $scope.domainRemove.busy = false; }); }, reset: function () { $scope.domainRemove.busy = false; $scope.domainRemove.error = null; $scope.domainRemove.domain = null; } }; $scope.changeDashboard = { busy: false, percent: 0, message: '', errorMessage: '', taskId: '', selectedDomain: null, adminDomain: null, stop: function () { Client.stopTask($scope.changeDashboard.taskId, function (error) { if (error) console.error(error); $scope.changeDashboard.busy = false; }); }, // this function is not called intentionally. currently, we do switching in two steps - prepare and set // if the user refreshed the UI in the middle of prepare, then it would be awkward to resume/call 'set' when the // user visits the UI the next time around. checkStatus: function () { Client.getLatestTaskByType('prepareDashboardDomain', function (error, task) { if (error) return console.error(error); if (!task) return; $scope.changeDashboard.taskId = task.id; $scope.changeDashboard.updateStatus(); }); }, updateStatus: function () { if (!$scope.changeDashboard.busy) return; // task got stopped Client.getTask($scope.changeDashboard.taskId, function (error, data) { if (error) return window.setTimeout($scope.changeDashboard.updateStatus, 5000); if (!data.active) { $scope.changeDashboard.busy = false; $scope.changeDashboard.message = ''; $scope.changeDashboard.percent = 100; // indicates that 'result' is valid $scope.changeDashboard.errorMessage = data.success ? '' : data.error.message; if (!$scope.changeDashboard.errorMessage) $scope.changeDashboard.setDashboardDomain(); return; } $scope.changeDashboard.busy = true; $scope.changeDashboard.percent = data.percent; $scope.changeDashboard.message = data.message; window.setTimeout($scope.changeDashboard.updateStatus, 500); }); }, setDashboardDomain: function () { Client.setDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error) { if (error) { console.error(error); $scope.changeDashboard.errorMessage = error.message; $scope.changeDashboard.busy = false; } else { window.location.href = 'https://my.' + $scope.changeDashboard.selectedDomain.domain; } }); }, change: function () { $scope.changeDashboard.busy = true; $scope.changeDashboard.message = 'Preparing dashboard domain'; $scope.changeDashboard.percent = 0; $scope.changeDashboard.errorMessage = ''; Client.prepareDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error, taskId) { if (error) { console.error(error); $scope.changeDashboard.errorMessage = error.message; $scope.changeDashboard.busy = false; } else { $scope.changeDashboard.taskId = taskId; $scope.changeDashboard.updateStatus(); } }); } }; Client.onReady(function () { refreshDomains(function (error) { if (error) return console.error(error); $scope.ready = true; }); $scope.renewCerts.checkStatus(); }); document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName'); document.getElementById('fallbackCertFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'certificateFile', 'certificateFileName'); document.getElementById('fallbackKeyFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'keyFile', 'keyFileName'); // setup all the dialog focus handling ['domainConfigureModal', 'domainRemoveModal'].forEach(function (id) { $('#' + id).on('shown.bs.modal', function () { $(this).find('[autofocus]:first').focus(); }); }); $('.modal-backdrop').remove(); }]);