2018-01-22 13:01:38 -08:00
|
|
|
'use strict';
|
|
|
|
|
|
2021-02-11 16:10:53 +01:00
|
|
|
/* global $, tld, angular, Clipboard */
|
2018-01-22 13:01:38 -08:00
|
|
|
|
|
|
|
|
// create main application module
|
2020-11-08 10:48:30 +01:00
|
|
|
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
2018-01-22 13:01:38 -08:00
|
|
|
|
|
|
|
|
app.filter('zoneName', function () {
|
|
|
|
|
return function (domain) {
|
|
|
|
|
return tld.getDomain(domain);
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', function ($scope, $http, $timeout, Client) {
|
|
|
|
|
var 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; }, {});
|
|
|
|
|
|
|
|
|
|
$scope.state = null; // 'initialized', 'waitingForDnsSetup', 'waitingForBox'
|
2019-05-22 11:10:41 -07:00
|
|
|
$scope.error = {};
|
2018-01-22 13:01:38 -08:00
|
|
|
$scope.provider = '';
|
|
|
|
|
$scope.showDNSSetup = false;
|
|
|
|
|
$scope.instanceId = '';
|
|
|
|
|
$scope.isDomain = false;
|
|
|
|
|
$scope.isSubdomain = false;
|
2019-11-11 11:07:52 -08:00
|
|
|
$scope.advancedVisible = false;
|
2020-06-16 15:28:58 +02:00
|
|
|
$scope.clipboardDone = false;
|
2020-12-21 22:36:43 -08:00
|
|
|
$scope.search = window.location.search;
|
|
|
|
|
$scope.setupToken = '';
|
2019-11-11 11:07:52 -08:00
|
|
|
|
2018-05-15 15:46:24 -07:00
|
|
|
$scope.tlsProvider = [
|
|
|
|
|
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
2018-09-26 18:14:16 -07:00
|
|
|
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
|
2018-05-15 15:46:24 -07:00
|
|
|
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
|
2018-09-26 18:14:16 -07:00
|
|
|
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
|
2018-08-27 13:17:07 -07:00
|
|
|
{ name: 'Self-Signed', value: 'fallback' }, // this is not 'Custom' because we don't allow user to upload certs during setup phase
|
2018-05-15 15:46:24 -07:00
|
|
|
];
|
2018-01-22 13:01:38 -08:00
|
|
|
|
2019-11-11 11:07:52 -08:00
|
|
|
$scope.sysinfo = {
|
|
|
|
|
provider: 'generic',
|
2022-07-22 13:00:20 +02:00
|
|
|
ipv4: '',
|
2019-11-11 11:07:52 -08:00
|
|
|
ifname: ''
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$scope.sysinfoProvider = [
|
|
|
|
|
{ name: 'Public IP', value: 'generic' },
|
|
|
|
|
{ name: 'Static IP Address', value: 'fixed' },
|
|
|
|
|
{ name: 'Network Interface', value: 'network-interface' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$scope.prettySysinfoProviderName = function (provider) {
|
|
|
|
|
switch (provider) {
|
|
|
|
|
case 'generic': return 'Public IP';
|
|
|
|
|
case 'fixed': return 'Static IP Address';
|
|
|
|
|
case 'network-interface': return 'Network Interface';
|
|
|
|
|
default: return 'Unknown';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2018-09-12 14:40:12 -07:00
|
|
|
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
|
|
|
|
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
|
|
|
|
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
|
|
|
|
};
|
|
|
|
|
|
2018-01-22 13:01:38 -08:00
|
|
|
// If we migrate the api origin we have to poll the new location
|
|
|
|
|
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
|
|
|
|
|
|
|
|
|
|
$scope.$watch('dnsCredentials.domain', function (newVal) {
|
|
|
|
|
if (!newVal) {
|
|
|
|
|
$scope.isDomain = false;
|
|
|
|
|
$scope.isSubdomain = false;
|
|
|
|
|
} else if (!tld.getDomain(newVal) || newVal[newVal.length-1] === '.') {
|
|
|
|
|
$scope.isDomain = false;
|
|
|
|
|
$scope.isSubdomain = false;
|
|
|
|
|
} else {
|
|
|
|
|
$scope.isDomain = true;
|
|
|
|
|
$scope.isSubdomain = tld.getDomain(newVal) !== newVal;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// keep in sync with domains.js
|
|
|
|
|
$scope.dnsProvider = [
|
|
|
|
|
{ name: 'AWS Route53', value: 'route53' },
|
2023-04-21 10:39:11 +02:00
|
|
|
{ name: 'Bunny', value: 'bunny' },
|
2018-10-26 15:03:27 -07:00
|
|
|
{ name: 'Cloudflare', value: 'cloudflare' },
|
2019-03-11 18:43:23 -07:00
|
|
|
{ name: 'DigitalOcean', value: 'digitalocean' },
|
2018-05-06 17:07:00 -07:00
|
|
|
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
2018-05-06 21:52:25 -07:00
|
|
|
{ name: 'GoDaddy', value: 'godaddy' },
|
2018-01-22 13:01:38 -08:00
|
|
|
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
2022-05-02 22:08:49 -07:00
|
|
|
{ name: 'Hetzner', value: 'hetzner' },
|
2020-11-16 22:21:07 -08:00
|
|
|
{ name: 'Linode', value: 'linode' },
|
2018-05-09 18:44:03 +02:00
|
|
|
{ name: 'Name.com', value: 'namecom' },
|
2019-01-22 16:45:48 +01:00
|
|
|
{ name: 'Namecheap', value: 'namecheap' },
|
2021-02-11 16:10:53 +01:00
|
|
|
{ name: 'Netcup', value: 'netcup' },
|
2023-03-16 10:24:31 +01:00
|
|
|
{ name: 'Porkbun', value: 'porkbun' },
|
2021-05-29 22:30:52 -07:00
|
|
|
{ name: 'Vultr', value: 'vultr' },
|
2018-01-22 13:01:38 -08:00
|
|
|
{ name: 'Wildcard', value: 'wildcard' },
|
|
|
|
|
{ name: 'Manual (not recommended)', value: 'manual' },
|
|
|
|
|
{ name: 'No-op (only for development)', value: 'noop' }
|
|
|
|
|
];
|
|
|
|
|
$scope.dnsCredentials = {
|
|
|
|
|
busy: false,
|
|
|
|
|
domain: '',
|
|
|
|
|
accessKeyId: '',
|
|
|
|
|
secretAccessKey: '',
|
|
|
|
|
gcdnsKey: { keyFileName: '', content: '' },
|
|
|
|
|
digitalOceanToken: '',
|
2018-05-06 17:07:00 -07:00
|
|
|
gandiApiKey: '',
|
2018-05-06 21:52:25 -07:00
|
|
|
cloudflareEmail: '',
|
|
|
|
|
cloudflareToken: '',
|
2020-01-01 16:02:50 -08:00
|
|
|
cloudflareTokenType: 'GlobalApiKey',
|
2023-02-11 08:40:36 +01:00
|
|
|
cloudflareDefaultProxyStatus: false,
|
2018-05-06 21:52:25 -07:00
|
|
|
godaddyApiKey: '',
|
|
|
|
|
godaddyApiSecret: '',
|
2020-03-12 17:13:21 -07:00
|
|
|
linodeToken: '',
|
2023-04-21 10:39:11 +02:00
|
|
|
bunnyAccessKey: '',
|
2022-05-02 22:08:49 -07:00
|
|
|
hetznerToken: '',
|
2021-05-29 22:30:52 -07:00
|
|
|
vultrToken: '',
|
2018-05-09 18:44:03 +02:00
|
|
|
nameComUsername: '',
|
|
|
|
|
nameComToken: '',
|
2019-01-22 16:45:48 +01:00
|
|
|
namecheapUsername: '',
|
|
|
|
|
namecheapApiKey: '',
|
2021-02-11 16:10:53 +01:00
|
|
|
netcupCustomerNumber: '',
|
|
|
|
|
netcupApiKey: '',
|
|
|
|
|
netcupApiPassword: '',
|
2023-03-16 10:24:31 +01:00
|
|
|
porkbunSecretapikey: '',
|
|
|
|
|
porkbunApikey: '',
|
|
|
|
|
|
2018-05-15 15:46:24 -07:00
|
|
|
provider: 'route53',
|
|
|
|
|
zoneName: '',
|
|
|
|
|
tlsConfig: {
|
2018-09-26 18:16:21 -07:00
|
|
|
provider: 'letsencrypt-prod-wildcard'
|
2020-08-15 18:35:51 -07:00
|
|
|
}
|
2018-01-22 13:01:38 -08:00
|
|
|
};
|
|
|
|
|
|
2018-09-26 22:21:50 -07:00
|
|
|
$scope.setDefaultTlsProvider = function () {
|
|
|
|
|
var dnsProvider = $scope.dnsCredentials.provider;
|
|
|
|
|
// wildcard LE won't work without automated DNS
|
|
|
|
|
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
|
|
|
|
|
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod';
|
|
|
|
|
} else {
|
|
|
|
|
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod-wildcard';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2018-01-22 13:01:38 -08:00
|
|
|
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]);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
|
|
|
|
|
|
|
|
|
|
$scope.setDnsCredentials = function () {
|
|
|
|
|
$scope.dnsCredentials.busy = true;
|
2019-05-22 11:10:41 -07:00
|
|
|
$scope.error = {};
|
2018-01-22 13:01:38 -08:00
|
|
|
|
|
|
|
|
var provider = $scope.dnsCredentials.provider;
|
|
|
|
|
|
2020-08-15 18:35:51 -07:00
|
|
|
var config = {};
|
2018-01-22 13:01:38 -08:00
|
|
|
|
|
|
|
|
if (provider === 'route53') {
|
2019-12-11 13:58:29 -08:00
|
|
|
config.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
|
|
|
|
config.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
2018-05-07 13:18:51 -07:00
|
|
|
} else if (provider === 'gcdns') {
|
2018-01-22 13:01:38 -08:00
|
|
|
try {
|
|
|
|
|
var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content);
|
2019-12-11 13:58:29 -08:00
|
|
|
config.projectId = serviceAccountKey.project_id;
|
|
|
|
|
config.credentials = {
|
2018-01-22 13:01:38 -08:00
|
|
|
client_email: serviceAccountKey.client_email,
|
|
|
|
|
private_key: serviceAccountKey.private_key
|
|
|
|
|
};
|
|
|
|
|
|
2019-12-11 13:58:29 -08:00
|
|
|
if (!config.projectId || !config.credentials || !config.credentials.client_email || !config.credentials.private_key) {
|
2020-03-03 11:07:56 -08:00
|
|
|
throw new Error('One or more fields are missing in the JSON');
|
2018-01-22 13:01:38 -08:00
|
|
|
}
|
2020-03-03 11:07:56 -08:00
|
|
|
} catch (e) {
|
|
|
|
|
$scope.error.dnsCredentials = 'Cannot parse Google Service Account Key: ' + e.message;
|
2018-01-22 13:01:38 -08:00
|
|
|
$scope.dnsCredentials.busy = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else if (provider === 'digitalocean') {
|
2019-12-11 13:58:29 -08:00
|
|
|
config.token = $scope.dnsCredentials.digitalOceanToken;
|
2018-05-06 17:07:00 -07:00
|
|
|
} else if (provider === 'gandi') {
|
2019-12-11 13:58:29 -08:00
|
|
|
config.token = $scope.dnsCredentials.gandiApiKey;
|
2018-05-06 21:52:25 -07:00
|
|
|
} else if (provider === 'godaddy') {
|
2019-12-11 13:58:29 -08:00
|
|
|
config.apiKey = $scope.dnsCredentials.godaddyApiKey;
|
|
|
|
|
config.apiSecret = $scope.dnsCredentials.godaddyApiSecret;
|
2018-01-22 13:01:38 -08:00
|
|
|
} else if (provider === 'cloudflare') {
|
2019-12-11 13:58:29 -08:00
|
|
|
config.email = $scope.dnsCredentials.cloudflareEmail;
|
|
|
|
|
config.token = $scope.dnsCredentials.cloudflareToken;
|
2020-01-01 16:02:50 -08:00
|
|
|
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
2023-02-11 08:40:36 +01:00
|
|
|
config.defaultProxyStatus = $scope.dnsCredentials.cloudflareDefaultProxyStatus;
|
2020-03-12 17:13:21 -07:00
|
|
|
} else if (provider === 'linode') {
|
|
|
|
|
config.token = $scope.dnsCredentials.linodeToken;
|
2023-04-21 10:39:11 +02:00
|
|
|
} else if (provider === 'bunny') {
|
|
|
|
|
config.token = $scope.dnsCredentials.bunnyAccessKey;
|
2022-05-02 22:08:49 -07:00
|
|
|
} else if (provider === 'hetzner') {
|
|
|
|
|
config.token = $scope.dnsCredentials.hetznerToken;
|
2021-05-29 22:30:52 -07:00
|
|
|
} else if (provider === 'vultr') {
|
|
|
|
|
config.token = $scope.dnsCredentials.vultrToken;
|
2018-05-09 18:44:03 +02:00
|
|
|
} else if (provider === 'namecom') {
|
2019-12-11 13:58:29 -08:00
|
|
|
config.username = $scope.dnsCredentials.nameComUsername;
|
|
|
|
|
config.token = $scope.dnsCredentials.nameComToken;
|
2019-01-22 16:45:48 +01:00
|
|
|
} else if (provider === 'namecheap') {
|
2019-12-11 13:58:29 -08:00
|
|
|
config.token = $scope.dnsCredentials.namecheapApiKey;
|
|
|
|
|
config.username = $scope.dnsCredentials.namecheapUsername;
|
2021-02-11 16:10:53 +01:00
|
|
|
} else if (provider === 'netcup') {
|
|
|
|
|
config.customerNumber = $scope.dnsCredentials.netcupCustomerNumber;
|
|
|
|
|
config.apiKey = $scope.dnsCredentials.netcupApiKey;
|
|
|
|
|
config.apiPassword = $scope.dnsCredentials.netcupApiPassword;
|
2023-03-16 10:24:31 +01:00
|
|
|
} else if (provider === 'porkbun') {
|
|
|
|
|
config.apikey = $scope.dnsCredentials.porkbunApikey;
|
|
|
|
|
config.secretapikey = $scope.dnsCredentials.porkbunSecretapikey;
|
2018-01-22 13:01:38 -08:00
|
|
|
}
|
|
|
|
|
|
2018-09-12 11:45:07 -07:00
|
|
|
var tlsConfig = {
|
|
|
|
|
provider: $scope.dnsCredentials.tlsConfig.provider,
|
|
|
|
|
wildcard: false
|
|
|
|
|
};
|
|
|
|
|
if ($scope.dnsCredentials.tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
|
|
|
|
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
|
|
|
|
tlsConfig.wildcard = true;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-11 11:07:52 -08:00
|
|
|
var sysinfoConfig = {
|
|
|
|
|
provider: $scope.sysinfo.provider
|
|
|
|
|
};
|
|
|
|
|
if ($scope.sysinfo.provider === 'fixed') {
|
2022-07-22 13:00:20 +02:00
|
|
|
sysinfoConfig.ipv4 = $scope.sysinfo.ipv4;
|
2019-11-11 11:07:52 -08:00
|
|
|
} else if ($scope.sysinfo.provider === 'network-interface') {
|
2019-11-11 16:06:29 -08:00
|
|
|
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
2019-11-11 11:07:52 -08:00
|
|
|
}
|
|
|
|
|
|
2019-12-11 13:58:29 -08:00
|
|
|
var data = {
|
2022-01-05 23:16:34 -08:00
|
|
|
domainConfig: {
|
2019-12-11 13:58:29 -08:00
|
|
|
domain: $scope.dnsCredentials.domain,
|
|
|
|
|
zoneName: $scope.dnsCredentials.zoneName,
|
|
|
|
|
provider: provider,
|
|
|
|
|
config: config,
|
|
|
|
|
tlsConfig: tlsConfig
|
|
|
|
|
},
|
|
|
|
|
sysinfoConfig: sysinfoConfig,
|
2020-12-21 22:36:43 -08:00
|
|
|
providerToken: $scope.instanceId,
|
|
|
|
|
setupToken: $scope.setupToken
|
2019-12-11 13:58:29 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Client.setup(data, function (error) {
|
2019-12-11 15:01:51 -08:00
|
|
|
if (error) {
|
2018-01-22 13:01:38 -08:00
|
|
|
$scope.dnsCredentials.busy = false;
|
2019-12-11 15:01:51 -08:00
|
|
|
if (error.statusCode === 422) {
|
2020-12-21 22:36:43 -08:00
|
|
|
if (provider === 'ami') {
|
|
|
|
|
$scope.error.ami = error.message;
|
|
|
|
|
} else {
|
|
|
|
|
$scope.error.setup = error.message;
|
|
|
|
|
}
|
2019-12-11 15:01:51 -08:00
|
|
|
} else {
|
|
|
|
|
$scope.error.dnsCredentials = error.message;
|
|
|
|
|
}
|
2018-01-22 13:01:38 -08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
waitForDnsSetup();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function waitForDnsSetup() {
|
|
|
|
|
$scope.state = 'waitingForDnsSetup';
|
|
|
|
|
|
|
|
|
|
Client.getStatus(function (error, status) {
|
2018-12-14 15:24:00 -08:00
|
|
|
if (!error && !status.setup.active) {
|
|
|
|
|
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
|
2019-05-22 11:10:41 -07:00
|
|
|
$scope.error.setup = status.setup.errorMessage;
|
2019-05-22 10:47:15 -07:00
|
|
|
$scope.state = 'initialized';
|
2019-05-22 11:10:41 -07:00
|
|
|
$scope.dnsCredentials.busy = false;
|
2018-12-14 15:24:00 -08:00
|
|
|
} else { // proceed to activation
|
2020-12-21 22:36:43 -08:00
|
|
|
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
|
2018-12-14 15:24:00 -08:00
|
|
|
}
|
|
|
|
|
return;
|
2018-01-22 13:01:38 -08:00
|
|
|
}
|
|
|
|
|
|
2018-12-15 16:08:44 -08:00
|
|
|
$scope.message = status.setup.message;
|
|
|
|
|
|
2018-01-22 13:01:38 -08:00
|
|
|
setTimeout(waitForDnsSetup, 5000);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initialize() {
|
|
|
|
|
Client.getStatus(function (error, status) {
|
|
|
|
|
if (error) {
|
|
|
|
|
// During domain migration, the box code restarts and can result in getStatus() failing temporarily
|
|
|
|
|
console.error(error);
|
|
|
|
|
$scope.state = 'waitingForBox';
|
|
|
|
|
return $timeout(initialize, 3000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// domain is currently like a lock flag
|
|
|
|
|
if (status.adminFqdn) return waitForDnsSetup();
|
|
|
|
|
|
2020-03-12 17:13:21 -07:00
|
|
|
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') {
|
|
|
|
|
$scope.dnsCredentials.provider = 'digitalocean';
|
2020-11-16 21:04:18 -08:00
|
|
|
} else if (status.provider === 'linode' || status.provider === 'linode-oneclick' || status.provider === 'linode-stackscript') {
|
|
|
|
|
$scope.dnsCredentials.provider = 'linode';
|
2021-05-29 22:30:52 -07:00
|
|
|
} else if (status.provider === 'vultr' || status.provider === 'vultr-mp') {
|
|
|
|
|
$scope.dnsCredentials.provider = 'vultr';
|
2020-03-12 17:13:21 -07:00
|
|
|
} else if (status.provider === 'gce') {
|
|
|
|
|
$scope.dnsCredentials.provider = 'gcdns';
|
|
|
|
|
} else if (status.provider === 'ami') {
|
|
|
|
|
$scope.dnsCredentials.provider = 'route53';
|
|
|
|
|
}
|
2018-01-22 13:01:38 -08:00
|
|
|
|
|
|
|
|
$scope.instanceId = search.instanceId;
|
2020-12-21 22:36:43 -08:00
|
|
|
$scope.setupToken = search.setupToken;
|
2018-01-22 13:01:38 -08:00
|
|
|
$scope.provider = status.provider;
|
|
|
|
|
$scope.state = 'initialized';
|
2020-03-09 16:08:55 -07:00
|
|
|
|
|
|
|
|
setTimeout(function () { $("[autofocus]:first").focus(); }, 100);
|
2018-01-22 13:01:38 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-16 15:28:58 +02:00
|
|
|
var clipboard = new Clipboard('.clipboard');
|
|
|
|
|
clipboard.on('success', function () {
|
|
|
|
|
$scope.$apply(function () { $scope.clipboardDone = true; });
|
|
|
|
|
$timeout(function () { $scope.clipboardDone = false; }, 5000);
|
|
|
|
|
});
|
|
|
|
|
|
2018-01-22 13:01:38 -08:00
|
|
|
initialize();
|
|
|
|
|
}]);
|