'use strict'; /* global asyncForEach:false */ /* global angular:false */ /* global $:false */ angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) { Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); $scope.config = Client.getConfig(); $scope.domains = []; $scope.ready = false; // 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: 'Name.com', value: 'namecom' }, { name: 'Namecheap', value: 'namecheap' }, { 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 'caas': return 'Managed Cloudron'; case 'route53': return 'AWS Route53'; case 'cloudflare': return 'Cloudflare'; case 'digitalocean': return 'DigitalOcean'; case 'gandi': return 'Gandi LiveDNS'; case 'namecom': return 'Name.com'; case 'namecheap': return 'Namecheap'; case 'gcdns': return 'Google Cloud'; case 'godaddy': return 'GoDaddy'; 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); asyncForEach(results, function (result, iteratorDone) { if (result.locked) { domains.push(result); return 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); }); }); } // 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: '', nameComToken: '', nameComUsername: '', namecheapUsername: '', namecheapApiKey: '', provider: 'route53', zoneName: '', hyphenatedSubdomains: false, 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 ($scope.domainConfigure.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, credentials: domain.config.credentials }); } $scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? 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.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.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; $scope.domainConfigure.hyphenatedSubdomains = !!domain.config.hyphenatedSubdomains; } 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 'fields_missing'; } } 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 === '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; } 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; } data.hyphenatedSubdomains = $scope.domainConfigure.hyphenatedSubdomains; 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.updateDomain.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.nameComToken = ''; $scope.domainConfigure.nameComUsername = ''; $scope.domainConfigure.namecheapApiKey = ''; $scope.domainConfigure.namecheapUsername = ''; $scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod'; $scope.domainConfigure.zoneName = ''; $scope.domainConfigure.hyphenatedSubdomains = false; $scope.domainConfigureForm.$setPristine(); $scope.domainConfigureForm.$setUntouched(); } }; $scope.renewCerts = { busy: false, percent: 0, message: '', errorMessage: '', taskId: '', checkStatus: function () { Client.getLatestTaskByType('renewcerts', 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 = ''; Client.renewCerts(null /* all domains */, 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.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(); } }); } }; $scope.dyndnsConfigure = { busy: false, success: false, error: '', currentState: false, enabled: false, refresh: function () { Client.getDynamicDnsConfig(function (error, enabled) { if (error) return console.error(error); $scope.dyndnsConfigure.currentState = enabled; $scope.dyndnsConfigure.enabled = enabled; }); }, submit: function () { $scope.dyndnsConfigure.busy = true; $scope.dyndnsConfigure.success = false; $scope.dyndnsConfigure.error = ''; Client.setDynamicDnsConfig($scope.dyndnsConfigure.enabled, function (error) { if (error) $scope.dyndnsConfigure.error = error.message; else $scope.dyndnsConfigure.currentState = $scope.dyndnsConfigure.enabled; $scope.dyndnsConfigure.busy = false; $scope.dyndnsConfigure.success = true; }); } }; Client.onReady(function () { refreshDomains(function (error) { if (error) return console.error(error); $scope.ready = true; if ($scope.config.uiSpec.domains.dynamicDns) { $scope.dyndnsConfigure.refresh(); } }); $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(); }]);