Merge remote-tracking branch 'origin/master' into feature/gcs
# Conflicts: # webadmin/src/views/certs.js # webadmin/src/views/settings.js
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form role="form" name="appConfigureForm" ng-submit="doConfigure()" autocomplete="off">
|
||||
<form role="form" name="appConfigureForm" ng-submit="appConfigure.submit()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.other">{{ appConfigure.error.other }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
|
||||
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
{{ appConfigure.usingAltDomain ? 'External Domain' : ((!appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-')) + config.fqdn) }}
|
||||
{{ appConfigure.usingAltDomain ? 'External Domain' : ((!appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-')) + appConfigure.domain) }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li>
|
||||
<a href="" ng-click="useAltDomain(false)">{{ config.fqdn }}</a>
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="useAltDomain(false, domain)">{{ domain }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" ng-click="useAltDomain(true)"><i class="fa fa-star"></i> External Domain</a>
|
||||
@@ -170,7 +170,7 @@
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || (appConfigure.usingAltDomain && !appConfigure.isAltDomainValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Save</button>
|
||||
<button type="button" class="btn btn-success" ng-click="appConfigure.submit()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || (appConfigure.usingAltDomain && !appConfigure.isAltDomainValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,9 +230,14 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInfo.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<h5 class="app-info-title">{{ appInfo.app.manifest.title }}</h5>
|
||||
<h5 class="app-info-title">
|
||||
{{ appInfo.app.manifest.title }}
|
||||
<span class="app-info-meta text-small">Package <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">v{{ appInfo.app.manifest.version }}</a> </span>
|
||||
</h5>
|
||||
<br/>
|
||||
<span class="app-info-meta">Package version <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">{{ appInfo.app.manifest.version }}</a> </span>
|
||||
<span class="app-info-meta" ng-show="appInfo.app.manifest.documentationUrl"><a target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}">Documentation</a> </span>
|
||||
<br/>
|
||||
<span class="app-info-meta">Last updated {{ appInfo.app.updateTime | prettyDate }}</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="app-postinstall-message" ng-hide="appInfo.app.manifest && appInfo.app.manifest.postInstallMessage">
|
||||
@@ -260,7 +265,7 @@
|
||||
<p>{{ appError.app.message | prettyAppMessage }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default pull-left" ng-click="showConfigure(appError.app)">Repair</button>
|
||||
<button type="button" class="btn btn-default pull-left" ng-click="appConfigure.show(appError.app)">Repair</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,7 +375,8 @@
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<img ng-src="{{app.iconUrl || 'img/appicon_fallback.png'}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
<div class="text-small text-muted" ng-show="domains.length > 1">{{ app.domain }}</div><br/>
|
||||
<img ng-src="{{app.iconUrl || 'img/appicon_fallback.png'}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -378,12 +384,12 @@
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.altDomain || app.location || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ app | installationStateLabel }}
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div class="status" ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -395,7 +401,7 @@
|
||||
<i class="fa fa-undo scale"></i>
|
||||
</a>
|
||||
|
||||
<a href="" ng-click="showConfigure(app)" ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure' || (app | installError)">
|
||||
<a href="" ng-click="appConfigure.show(app)" ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure' || (app | installError)">
|
||||
<i ng-hide="(app | installError)" class="fa fa-pencil scale"></i>
|
||||
<i ng-show="(app | installError)" class="fa fa-wrench scale"></i>
|
||||
</a>
|
||||
@@ -418,11 +424,11 @@
|
||||
</div>
|
||||
|
||||
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
|
||||
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-pencil scale"></i></a>
|
||||
<a href="" ng-click="appConfigure.show(app)" title="Configure App"><i class="fa fa-pencil scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="app | installError">
|
||||
<a href="" ng-click="showConfigure(app)" title="Repair App"><i class="fa fa-wrench scale"></i></a>
|
||||
<a href="" ng-click="appConfigure.show(app)" title="Repair App"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
+133
-128
@@ -7,7 +7,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.dnsConfig = {};
|
||||
$scope.domains = [];
|
||||
$scope.groups = [];
|
||||
$scope.users = [];
|
||||
$scope.mailConfig = {};
|
||||
@@ -17,6 +17,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
busy: false,
|
||||
error: {},
|
||||
app: {},
|
||||
domain: '',
|
||||
location: '',
|
||||
usingAltDomain: false,
|
||||
advancedVisible: false,
|
||||
@@ -51,6 +52,129 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
|
||||
isAltDomainNaked: function () {
|
||||
return ngTld.isNakedDomain($scope.appConfigure.location);
|
||||
},
|
||||
|
||||
show: function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.altDomain || app.location;
|
||||
$scope.appConfigure.domain = app.domain;
|
||||
$scope.appConfigure.usingAltDomain = !!app.altDomain;
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope. Option = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
|
||||
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
|
||||
$scope.appConfigure.robotsTxt = app.robotsTxt;
|
||||
$scope.appConfigure.enableBackup = app.enableBackup;
|
||||
|
||||
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
|
||||
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
|
||||
$scope.appConfigure.memoryTicks = [ ];
|
||||
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2)));
|
||||
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
|
||||
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.appConfigure.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) {
|
||||
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
|
||||
}
|
||||
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.accessRestriction = { users: [], groups: [] };
|
||||
|
||||
if (app.accessRestriction) {
|
||||
var userSet = { };
|
||||
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
||||
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.appConfigure.accessRestriction.users.push(u); });
|
||||
|
||||
var groupSet = { };
|
||||
app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
||||
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.appConfigure.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
if (app.portBindings && app.portBindings[env]) {
|
||||
$scope.appConfigure.portBindings[env] = app.portBindings[env];
|
||||
$scope.appConfigure.portBindingsEnabled[env] = true;
|
||||
} else {
|
||||
$scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0;
|
||||
$scope.appConfigure.portBindingsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$('#appConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appConfigure.busy = true;
|
||||
$scope.appConfigure.error.other = null;
|
||||
$scope.appConfigure.error.location = null;
|
||||
$scope.appConfigure.error.xFrameOptions = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appConfigure.portBindings) {
|
||||
if ($scope.appConfigure.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appConfigure.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appConfigure.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location,
|
||||
altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null,
|
||||
domain: $scope.appConfigure.usingAltDomain ? undefined : $scope.appConfigure.domain,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN',
|
||||
memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit,
|
||||
robotsTxt: $scope.appConfigure.robotsTxt,
|
||||
enableBackup: $scope.appConfigure.enableBackup
|
||||
};
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, data, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appConfigure.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appConfigure.error.location = 'This name is already taken.';
|
||||
$scope.appConfigureForm.location.$setPristine();
|
||||
$('#appConfigureLocationInput').focus();
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appConfigure.error.cert = error.message;
|
||||
$scope.appConfigure.certificateFileName = '';
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) {
|
||||
$scope.appConfigure.error.xFrameOptions = error.message;
|
||||
$scope.appConfigureForm.xFrameOptions.$setPristine();
|
||||
$('#appConfigureXFrameOptionsInput').focus();
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
|
||||
$('#appConfigureModal').modal('hide');
|
||||
|
||||
$scope.reset();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,6 +269,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
// reset configure dialog
|
||||
$scope.appConfigure.error = {};
|
||||
$scope.appConfigure.app = {};
|
||||
$scope.appConfigure.domain = '';
|
||||
$scope.appConfigure.location = '';
|
||||
$scope.appConfigure.advancedVisible = false;
|
||||
$scope.appConfigure.usingAltDomain = false;
|
||||
@@ -218,8 +343,9 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.useAltDomain = function (use) {
|
||||
$scope.useAltDomain = function (use, domain) {
|
||||
$scope.appConfigure.usingAltDomain = use;
|
||||
$scope.appConfigure.domain = domain;
|
||||
|
||||
if (use) {
|
||||
$scope.appConfigure.location = '';
|
||||
@@ -228,127 +354,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showConfigure = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.altDomain || app.location;
|
||||
$scope.appConfigure.usingAltDomain = !!app.altDomain;
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope. Option = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
|
||||
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
|
||||
$scope.appConfigure.robotsTxt = app.robotsTxt;
|
||||
$scope.appConfigure.enableBackup = app.enableBackup;
|
||||
|
||||
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
|
||||
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
|
||||
$scope.appConfigure.memoryTicks = [ ];
|
||||
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2)));
|
||||
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
|
||||
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.appConfigure.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) {
|
||||
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
|
||||
}
|
||||
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.accessRestriction = { users: [], groups: [] };
|
||||
|
||||
if (app.accessRestriction) {
|
||||
var userSet = { };
|
||||
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
||||
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.appConfigure.accessRestriction.users.push(u); });
|
||||
|
||||
var groupSet = { };
|
||||
app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
||||
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.appConfigure.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
if (app.portBindings && app.portBindings[env]) {
|
||||
$scope.appConfigure.portBindings[env] = app.portBindings[env];
|
||||
$scope.appConfigure.portBindingsEnabled[env] = true;
|
||||
} else {
|
||||
$scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0;
|
||||
$scope.appConfigure.portBindingsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$('#appConfigureModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doConfigure = function () {
|
||||
$scope.appConfigure.busy = true;
|
||||
$scope.appConfigure.error.other = null;
|
||||
$scope.appConfigure.error.location = null;
|
||||
$scope.appConfigure.error.xFrameOptions = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appConfigure.portBindings) {
|
||||
if ($scope.appConfigure.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appConfigure.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appConfigure.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location,
|
||||
altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN',
|
||||
memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit,
|
||||
robotsTxt: $scope.appConfigure.robotsTxt,
|
||||
enableBackup: $scope.appConfigure.enableBackup
|
||||
};
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, data, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appConfigure.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appConfigure.error.location = 'This name is already taken.';
|
||||
$scope.appConfigureForm.location.$setPristine();
|
||||
$('#appConfigureLocationInput').focus();
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appConfigure.error.cert = error.message;
|
||||
$scope.appConfigure.certificateFileName = '';
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) {
|
||||
$scope.appConfigure.error.xFrameOptions = error.message;
|
||||
$scope.appConfigureForm.xFrameOptions.$setPristine();
|
||||
$('#appConfigureXFrameOptionsInput').focus();
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
|
||||
$('#appConfigureModal').modal('hide');
|
||||
|
||||
$scope.reset();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showInformation = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
@@ -465,14 +470,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
}
|
||||
|
||||
function fetchDnsConfig() {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
function getDomains() {
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchDnsConfig, 5000);
|
||||
return $timeout(getDomains, 5000);
|
||||
}
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
$scope.domains = result.map(function (d) { return d.domain; });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -499,7 +504,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
if ($scope.user.admin) {
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
fetchDnsConfig();
|
||||
getDomains();
|
||||
getMailConfig();
|
||||
getBackupConfig();
|
||||
}
|
||||
|
||||
@@ -21,8 +21,16 @@
|
||||
<label class="control-label" for="appInstallLocationInput">Location {{ appInstall.error.location }} </label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.location" id="appInstallLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
|
||||
<div class="input-group-addon">
|
||||
{{ !appInstall.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
{{ (config.isCustomDomain ? '.' : '-') + appInstall.domain }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="appInstall.domain = domain">{{ domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,7 +286,6 @@
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'project' }" category="project"><i class="fa fa-line-chart"></i> Project Management</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki"><i class="fa fa-wikipedia-w"></i> Wiki</a>
|
||||
<br/>
|
||||
<!-- <a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'testing' }" category="testing" ng-show="config.developerMode">Testing</a> -->
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
@@ -12,7 +12,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.users = [];
|
||||
$scope.groups = [];
|
||||
$scope.dnsConfig = {};
|
||||
$scope.domains = [];
|
||||
$scope.category = '';
|
||||
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
||||
$scope.searchString = '';
|
||||
@@ -37,6 +37,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
error: {},
|
||||
app: {},
|
||||
location: '',
|
||||
domain: '',
|
||||
portBindings: {},
|
||||
mediaLinks: [],
|
||||
certificateFile: null,
|
||||
@@ -57,6 +58,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.location = '';
|
||||
$scope.appInstall.domain = '';
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.state = 'appInfo';
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
@@ -102,6 +104,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.location = app.location;
|
||||
$scope.appInstall.domain = $scope.config.fqdn; // FIXME needs to come from domains dropdown
|
||||
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
@@ -145,6 +148,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
var data = {
|
||||
location: $scope.appInstall.location || '',
|
||||
domain: $scope.appInstall.domain,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
@@ -492,14 +496,14 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
}
|
||||
|
||||
function fetchDnsConfig() {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
function getDomains() {
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchDnsConfig, 5000);
|
||||
return $timeout(getDomains, 5000);
|
||||
}
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
$scope.domains = result.map(function (d) { return d.domain; });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -557,7 +561,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
fetchDnsConfig();
|
||||
getDomains();
|
||||
getMailConfig();
|
||||
|
||||
fetchAppstoreConfig(function (error) {
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
<div class="modal fade" id="dnsCredentialsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Configure DNS</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="dnsCredentialsForm" role="form" novalidate ng-submit="setDnsCredentials()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</p>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
|
||||
<label class="control-label" for="customDomainId">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" placeholder="example.com" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="dnsCredentialsProvider">DNS API provider</label>
|
||||
<select class="form-control" id="dnsCredentialsProvider" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label" for="dnsCredentialsAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" id="dnsCredentialsAccessKeyId" name="accessKeyId" ng-disabled="dnsCredentials.busy" ng-minlength="16" ng-maxlength="32" ng-required="dnsCredentials.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label" for="dnsCredentialsDigitalOceanToken">DigitalOcean token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" id="dnsCredentialsDigitalOceanToken" name="digitalOceanToken" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" for="dnsCredentialsCloudflareToken">Cloudflare token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" id="dnsCredentialsCloudflareToken" name="cloudflareToken" placeholder="API Key" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" for="dnsCredentialsCloudflareEmail">Cloudflare email</label>
|
||||
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" id="dnsCredentialsCloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- this will be autofilled by most browsers regardless of the attribute, since the next field is a password field.... -->
|
||||
<input type="text" class="form-control hide">
|
||||
|
||||
<!-- all provider -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<label class="control-label" for="dnsCredentialsPassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="dnsCredentials.password" id="dnsCredentialsPassword" name="password" ng-disabled="dnsCredentials.busy" required>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="dnsCredentialsForm.$invalid || dnsCredentials.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'route53'">
|
||||
This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
This domain must be hosted on <a href="https://console.cloud.google.com/net-services/dns/zones" target="_blank">Google Cloud DNS</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
This domain must be hosted on <a href="https://www.cloudflare.com" target="_blank">Cloudflare</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ dnsCredentials.customDomain || 'example.com' }}</b> and <b>{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'manual'">
|
||||
Setup an <i>A</i> record for <b>{{ config.adminLocation }}.{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="setDnsCredentials()" ng-disabled="dnsCredentialsForm.$invalid || dnsCredentials.busy">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i>
|
||||
<span ng-show="dnsCredentials.customDomain === config.fqdn">Save</span>
|
||||
<span ng-show="dnsCredentials.customDomain !== config.fqdn">Change Domain</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Domain & Certificates</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Domain</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!config.isCustomDomain">To use a custom domain, configure your domain to use <a target="_blank" href="https://aws.amazon.com/route53/">Route53.</a> Moving to a custom domain will retain all your apps and data and will take around 15 minutes.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Domain name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.fqdn }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">DNS provider</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.provider }}</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && !dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider is configured. All DNS records need to be setup manually.
|
||||
To avoid manual setup for each installed app, set a DNS API provider.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
Wildcard DNS provider is configured. Always ensure there is a wildcard DNS record for this server's IP.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'noop'">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider configured. All DNS records need to be setup manually and all DNS checks are skipped.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Access key id</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId || 'unset' }}</td>
|
||||
</tr>
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Secret access key</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.secretAccessKey"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'digitalocean'">
|
||||
<td class="text-muted" style="vertical-align: top;">DigitalOcean token</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.token"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<!-- add some space -->
|
||||
<tr>
|
||||
<td><br/></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;"></td>
|
||||
<td class="text-right" style="vertical-align: top;"><button class="btn btn-outline btn-primary" ng-click="showChangeDnsCredentials()">Change</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>SSL Certificates</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-show="!config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
Certificates can only by set for custom domains.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
<form name="defaultCertForm" ng-submit="setDefaultCert()">
|
||||
<fieldset>
|
||||
<p>Certificates are automatically obtained and renewed from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>. See the current rate limit <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank">here</a>.</p>
|
||||
<br/>
|
||||
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
|
||||
<p>This wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.</p>
|
||||
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="defaultCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row hide">
|
||||
<div class="col-md-12">
|
||||
<form name="adminCertForm" ng-submit="setAdminCert()">
|
||||
<fieldset>
|
||||
<label class="control-label" for="adminCertInput">Settings Certificate</label>
|
||||
<p>This certificate will be used for this Settings application.</p>
|
||||
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="adminCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,258 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('CertsController', ['$scope', '$location', 'Client', 'ngTld', function ($scope, $location, Client, ngTld) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = null;
|
||||
|
||||
// keep in sync with setupdns.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Cloudflare (DNS only)', value: 'cloudflare' },
|
||||
{ name: 'Digital Ocean', value: 'digitalocean' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
|
||||
$scope.defaultCert = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.adminCert = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.dnsCredentials = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
customDomain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
provider: 'route53',
|
||||
password: ''
|
||||
};
|
||||
|
||||
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('defaultCertFileInput').onchange = readFileLocally($scope.defaultCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('defaultKeyFileInput').onchange = readFileLocally($scope.defaultCert, 'keyFile', 'keyFileName');
|
||||
document.getElementById('adminCertFileInput').onchange = readFileLocally($scope.adminCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('adminKeyFileInput').onchange = readFileLocally($scope.adminCert, 'keyFile', 'keyFileName');
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
$scope.setDefaultCert = function () {
|
||||
$scope.defaultCert.busy = true;
|
||||
$scope.defaultCert.error = null;
|
||||
$scope.defaultCert.success = false;
|
||||
|
||||
Client.setCertificate($scope.defaultCert.certificateFile, $scope.defaultCert.keyFile, function (error) {
|
||||
if (error) {
|
||||
$scope.defaultCert.error = error.message;
|
||||
} else {
|
||||
$scope.defaultCert.success = true;
|
||||
$scope.defaultCert.certificateFileName = '';
|
||||
$scope.defaultCert.keyFileName = '';
|
||||
}
|
||||
|
||||
$scope.defaultCert.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setAdminCert = function () {
|
||||
$scope.adminCert.busy = true;
|
||||
$scope.adminCert.error = null;
|
||||
$scope.adminCert.success = false;
|
||||
|
||||
Client.setAdminCertificate($scope.adminCert.certificateFile, $scope.adminCert.keyFile, function (error) {
|
||||
if (error) {
|
||||
$scope.adminCert.error = error.message;
|
||||
} else {
|
||||
$scope.adminCert.success = true;
|
||||
$scope.adminCert.certificateFileName = '';
|
||||
$scope.adminCert.keyFileName = '';
|
||||
}
|
||||
|
||||
$scope.adminCert.busy = false;
|
||||
|
||||
// attempt to reload to make the browser get the new certs
|
||||
window.location.reload(true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.dnsCredentials.error = null;
|
||||
$scope.dnsCredentials.success = false;
|
||||
|
||||
var migrateDomain = $scope.dnsCredentials.customDomain !== $scope.config.fqdn;
|
||||
|
||||
var data = {
|
||||
provider: $scope.dnsCredentials.provider
|
||||
};
|
||||
|
||||
// special case the wildcard provider
|
||||
if (data.provider === 'wildcard') {
|
||||
data.provider = 'manual';
|
||||
data.wildcard = true;
|
||||
}
|
||||
|
||||
if (data.provider === 'route53') {
|
||||
data.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
data.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
} else if (data.provider === 'gcdns'){
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.dnsCredentials.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.dnsCredentials.error = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.dnsCredentials.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (data.provider === 'digitalocean') {
|
||||
data.token = $scope.dnsCredentials.digitalOceanToken;
|
||||
} else if (data.provider === 'cloudflare') {
|
||||
data.token = $scope.dnsCredentials.cloudflareToken;
|
||||
data.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
}
|
||||
|
||||
var func;
|
||||
if (migrateDomain) {
|
||||
data.domain = $scope.dnsCredentials.customDomain;
|
||||
func = Client.migrate.bind(Client, data, $scope.dnsCredentials.password);
|
||||
} else {
|
||||
func = Client.setDnsConfig.bind(Client, data);
|
||||
}
|
||||
|
||||
func(function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.error = error.message;
|
||||
} else {
|
||||
$scope.dnsCredentials.success = true;
|
||||
|
||||
$('#dnsCredentialsModal').modal('hide');
|
||||
|
||||
dnsCredentialsReset();
|
||||
|
||||
if (migrateDomain) window.location.href = '/update.html';
|
||||
}
|
||||
|
||||
$scope.dnsCredentials.busy = false;
|
||||
|
||||
// reload the dns config
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function dnsCredentialsReset() {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
$scope.dnsCredentials.success = false;
|
||||
$scope.dnsCredentials.error = null;
|
||||
|
||||
$scope.dnsCredentials.provider = '';
|
||||
$scope.dnsCredentials.customDomain = '';
|
||||
$scope.dnsCredentials.accessKeyId = '';
|
||||
$scope.dnsCredentials.secretAccessKey = '';
|
||||
$scope.dnsCredentials.gcdnsKey.keyFileName = '';
|
||||
$scope.dnsCredentials.gcdnsKey.content = '';
|
||||
$scope.dnsCredentials.digitalOceanToken = '';
|
||||
$scope.dnsCredentials.cloudflareToken = '';
|
||||
$scope.dnsCredentials.cloudflareEmail = '';
|
||||
$scope.dnsCredentials.password = '';
|
||||
|
||||
$scope.dnsCredentialsForm.$setPristine();
|
||||
$scope.dnsCredentialsForm.$setUntouched();
|
||||
|
||||
$('#customDomainId').focus();
|
||||
}
|
||||
|
||||
$scope.showChangeDnsCredentials = function () {
|
||||
dnsCredentialsReset();
|
||||
|
||||
// clear the input box for non-custom domain
|
||||
$scope.dnsCredentials.customDomain = $scope.config.isCustomDomain ? $scope.config.fqdn : '';
|
||||
$scope.dnsCredentials.accessKeyId = $scope.dnsConfig.accessKeyId;
|
||||
$scope.dnsCredentials.secretAccessKey = $scope.dnsConfig.secretAccessKey;
|
||||
|
||||
$scope.dnsCredentials.gcdnsKey.keyFileName = '';
|
||||
$scope.dnsCredentials.gcdnsKey.content = '';
|
||||
if ($scope.dnsConfig.provider === 'gcdns') {
|
||||
$scope.dnsCredentials.gcdnsKey.keyFileName = $scope.dnsConfig.credentials && $scope.dnsConfig.credentials.client_email;
|
||||
$scope.dnsCredentials.gcdnsKey.content = JSON.stringify({
|
||||
"project_id": $scope.dnsConfig.projectId,
|
||||
"credentials": $scope.dnsConfig.credentials
|
||||
});
|
||||
}
|
||||
$scope.dnsCredentials.digitalOceanToken = $scope.dnsConfig.provider === 'digitalocean' ? $scope.dnsConfig.token : '';
|
||||
$scope.dnsCredentials.cloudflareToken = $scope.dnsConfig.provider === 'cloudflare' ? $scope.dnsConfig.token : '';
|
||||
$scope.dnsCredentials.cloudflareEmail = $scope.dnsConfig.email;
|
||||
|
||||
$scope.dnsCredentials.provider = $scope.dnsConfig.provider === 'caas' ? 'route53' : $scope.dnsConfig.provider;
|
||||
$scope.dnsCredentials.provider = ($scope.dnsCredentials.provider === 'manual' && $scope.dnsConfig.wildcard) ? 'wildcard' : $scope.dnsCredentials.provider;
|
||||
|
||||
$('#dnsCredentialsModal').modal('show');
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['dnsCredentialsModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -68,8 +68,8 @@
|
||||
<div class="logs-controls">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<uib-tabset active="active">
|
||||
<uib-tab index="1" heading="Terminal" select="showTerminal()"></uib-tab>
|
||||
<uib-tab index="0" heading="Logs" select="showLogs()"></uib-tab>
|
||||
<uib-tab index="1" heading="Terminal" select="showTerminal()"></uib-tab>
|
||||
</uib-tabset>
|
||||
<select class="form-control pull-right inline" ng-options="log.name for log in logs track by log.value" ng-model="selected"></select>
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<div class="modal fade" id="domainConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-show="domainConfigure.adding">Add Domain</h4>
|
||||
<h4 class="modal-title" ng-hide="domainConfigure.adding">Configure {{ domainConfigure.domain.domain }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="domainConfigureForm" role="form" novalidate ng-submit="domainConfigure.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="domainConfigure.error">{{ domainConfigure.error }}</p>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': domainConfigureForm.newDomain.$invalid }" ng-show="domainConfigure.adding">
|
||||
<label class="control-label">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.newDomain" name="newDomain" ng-disabled="domainConfigure.busy" placeholder="example.com" ng-required="domainConfigure.adding" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS API provider</label>
|
||||
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.accessKeyId" name="accessKeyId" ng-disabled="domainConfigure.busy" ng-minlength="16" ng-maxlength="32" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.secretAccessKey" name="secretAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gcdns'">
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="domainConfigure.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
<label class="control-label">DigitalOcean token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.digitalOceanToken" name="digitalOceanToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">Cloudflare token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.cloudflareToken" name="cloudflareToken" placeholder="API Key" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">Cloudflare email</label>
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'route53'">
|
||||
This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'gcdns'">
|
||||
This domain must be hosted on <a href="https://console.cloud.google.com/net-services/dns/zones" target="_blank">Google Cloud DNS</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
This domain must be hosted on <a href="https://www.cloudflare.com" target="_blank">Cloudflare</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> and <b>{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'manual'">
|
||||
All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<!-- Wildcard certificates -->
|
||||
<label class="control-label">Fallback Certificate (optional)</label>
|
||||
<p>
|
||||
Certificates are automatically obtained and renewed from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>. See the current rate limit <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank">here</a>.
|
||||
If provided, this wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.
|
||||
</p>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.cert.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="domainConfigure.fallbackCert.certificateFileName" name="cert" onclick="getElementById('fallbackCertFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('fallbackCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.key.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="domainConfigure.fallbackCert.keyFileName" id="fallbackKeyInput" name="key" onclick="getElementById('fallbackKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('fallbackKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="domainConfigure.busy"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal domain remove -->
|
||||
<div class="modal fade" id="domainRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really remove {{ domainRemove.domain.domain }} ?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form role="form" name="domainRemoveForm" ng-submit="domainRemove.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (domainRemoveForm.password.$dirty && domainRemoveForm.password.$invalid) || (!domainRemoveForm.password.$dirty && domainRemove.error) }">
|
||||
<label class="control-label">Provide your password to confirm this action</label>
|
||||
<div class="control-label" ng-show="(domainRemoveForm.password.$dirty && domainRemoveForm.password.$invalid) || (!domainRemoveForm.password.$dirty && domainRemove.error)">
|
||||
<small ng-show=" domainRemoveForm.password.$dirty && domainRemoveForm.password.$invalid">Password required</small>
|
||||
<small ng-show="!domainRemoveForm.password.$dirty && domainRemove.error">{{ domainRemove.error }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="domainRemove.password" id="domainRemovePasswordInput" name="password" required autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainRemoveForm.$invalid || busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="domainRemove.submit()" ng-disabled="domainRemoveForm.$invalid || domainRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="domainRemove.busy"></i> Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Domains <button class="btn btn-primary btn-outline pull-right" ng-hide="true" ng-click="domainConfigure.show()"><i class="fa fa-plus"></i> Add Domain</button></h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th class="text-left hidden-xs hidden-sm">Provider</th>
|
||||
<th style="width: 100px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains">
|
||||
<td class="elide-table-cell">
|
||||
{{ domain.domain }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm">
|
||||
{{ domain.config.provider }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" ng-show="domain.config.provider !== 'caas'" title="Edit Domain"><i class="fa fa-pencil"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-hide="true" ng-click="domainRemove.show(domain)" ng-show="domain.config.provider !== 'caas'" title="Remove Domain"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,261 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', 'ngTld', function ($scope, $location, Client, ngTld) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = null;
|
||||
$scope.domains = [];
|
||||
$scope.ready = false;
|
||||
|
||||
// keep in sync with setupdns.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Cloudflare (DNS only)', value: 'cloudflare' },
|
||||
{ name: 'Digital Ocean', value: 'digitalocean' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
|
||||
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]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// We reused configure also for adding domains to avoid much code duplication
|
||||
$scope.domainConfigure = {
|
||||
adding: false,
|
||||
error: null,
|
||||
busy: false,
|
||||
domain: null,
|
||||
|
||||
// form model
|
||||
newDomain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
provider: 'route53',
|
||||
|
||||
fallbackCert: {
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
},
|
||||
|
||||
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.config.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareToken = domain.config.provider === 'cloudflare' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareEmail = domain.config.email;
|
||||
|
||||
$scope.domainConfigure.provider = domain.config.provider;
|
||||
$scope.domainConfigure.provider = ($scope.domainConfigure.provider === 'manual' && domain.config.wildcard) ? 'wildcard' : domain.config.provider;
|
||||
} else {
|
||||
$scope.domainConfigure.adding = true;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainConfigure.busy = true;
|
||||
$scope.domainConfigure.error = null;
|
||||
|
||||
var data = {
|
||||
provider: $scope.domainConfigure.provider
|
||||
};
|
||||
|
||||
// special case the wildcard provider
|
||||
if (data.provider === 'wildcard') {
|
||||
data.provider = 'manual';
|
||||
data.wildcard = true;
|
||||
}
|
||||
|
||||
if (data.provider === 'route53') {
|
||||
data.accessKeyId = $scope.domainConfigure.accessKeyId;
|
||||
data.secretAccessKey = $scope.domainConfigure.secretAccessKey;
|
||||
} else if (data.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 (data.provider === 'digitalocean') {
|
||||
data.token = $scope.domainConfigure.digitalOceanToken;
|
||||
} else if (data.provider === 'cloudflare') {
|
||||
data.token = $scope.domainConfigure.cloudflareToken;
|
||||
data.email = $scope.domainConfigure.cloudflareEmail;
|
||||
}
|
||||
|
||||
var fallbackCertificate = null;
|
||||
if ($scope.domainConfigure.fallbackCert.certificateFile && $scope.domainConfigure.fallbackCert.keyFile) {
|
||||
fallbackCertificate = {
|
||||
cert: $scope.domainConfigure.fallbackCert.certificateFile,
|
||||
key: $scope.domainConfigure.fallbackCert.keyFile
|
||||
};
|
||||
}
|
||||
|
||||
// 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, data, fallbackCertificate);
|
||||
else func = Client.updateDomain.bind(Client, $scope.domainConfigure.domain.domain, data, fallbackCertificate);
|
||||
|
||||
func(function (error) {
|
||||
$scope.domainConfigure.busy = false;
|
||||
if (error) {
|
||||
$scope.domainConfigure.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('hide');
|
||||
$scope.domainConfigure.reset();
|
||||
|
||||
// reload the domains
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainConfigure.adding = 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.cloudflareToken = '';
|
||||
$scope.domainConfigure.cloudflareEmail = '';
|
||||
|
||||
$scope.domainConfigureForm.$setPristine();
|
||||
$scope.domainConfigureForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
domain: null,
|
||||
password: '',
|
||||
|
||||
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, $scope.domainRemove.password, function (error) {
|
||||
if (error && (error.statusCode === 403 || error.statusCode === 409)) {
|
||||
$scope.domainRemove.password = '';
|
||||
$scope.domainRemove.error = error.message;
|
||||
$scope.domainRemoveForm.password.$setPristine();
|
||||
$('#domainRemovePasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#domainRemoveModal').modal('hide');
|
||||
$scope.domainRemove.reset();
|
||||
|
||||
// reload the domains
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.domainRemove.busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainRemove.busy = false;
|
||||
$scope.domainRemove.error = null;
|
||||
$scope.domainRemove.domain = null;
|
||||
$scope.domainRemove.password = '';
|
||||
|
||||
$scope.domainRemoveForm.$setPristine();
|
||||
$scope.domainRemoveForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
}]);
|
||||
@@ -27,18 +27,32 @@
|
||||
</div>
|
||||
|
||||
<!-- Test email sent -->
|
||||
<div class="modal fade" id="testEmailSent" tabindex="-1" role="dialog">
|
||||
<div class="modal fade" id="testEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Test Email Sent</h4>
|
||||
<h4 class="modal-title">Send test email</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
A test email was sent to {{ user.email }}. Please check the inbox of this account and verify the email was delivered.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Ok</button>
|
||||
<form name="testEmailForm" role="form" novalidate ng-submit="testEmail.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="testEmail.error">{{ testEmail.error.generic }}</p>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': testEmail.error.key }">
|
||||
<label class="control-label" for="inputTestEmailKey">Email to</label>
|
||||
<input type="text" class="form-control" ng-model="testEmail.mailTo" id="inputTestMailTo" name="mailTo" ng-disabled="testEmail.busy" placeholder="Email address" autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="testEmailForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="testEmail.busy"></i><span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,12 +195,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas' && currentRelay.provider === 'cloudron-smtp'">
|
||||
<h3>DNS Records</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas' && currentRelay.provider === 'cloudron-smtp'">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12">
|
||||
Set the following DNS records to guarantee email delivery:
|
||||
|
||||
@@ -196,7 +211,7 @@
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value] && (mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX'))">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-hide="email.refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
@@ -211,8 +226,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
@@ -226,13 +243,13 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
|
||||
<i ng-hide="email.refreshBusy" ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_outbound_smtp">
|
||||
Outbound SMTP
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!relay.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_port" class="panel-collapse collapse">
|
||||
<div id="collapse_outbound_smtp" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ relay.value }} </b> </p>
|
||||
</div>
|
||||
@@ -240,10 +257,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="rbl">
|
||||
<div class="row" ng-show="currentRelay.provider === 'cloudron-smtp'">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-hide="email.refreshBusy" ng-class="rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">
|
||||
IP Address Blacklist Check
|
||||
</a>
|
||||
@@ -262,7 +279,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button class="btn btn-primary pull-left" ng-click="sendTestEmail()">Send Test Email</button>
|
||||
<button class="btn btn-primary pull-left" ng-click="testEmail.show()">Send Test Email</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+86
-15
@@ -7,9 +7,16 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = {};
|
||||
$scope.currentRelay = {};
|
||||
$scope.relay = {};
|
||||
$scope.rbl = null;
|
||||
$scope.expectedDnsRecords = {};
|
||||
$scope.expectedDnsRecords = {
|
||||
mx: { },
|
||||
dkim: { },
|
||||
spf: { },
|
||||
dmarc: { },
|
||||
ptr: { }
|
||||
};
|
||||
$scope.expectedDnsRecordsTypes = [
|
||||
{ name: 'MX', value: 'mx' },
|
||||
{ name: 'DKIM', value: 'dkim' },
|
||||
@@ -81,6 +88,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
refresh: function () {
|
||||
$scope.email.refreshBusy = true;
|
||||
|
||||
collapseDnsRecords();
|
||||
|
||||
showExpectedDnsRecords(function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
@@ -150,20 +159,56 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
|
||||
Client.setMailRelay(data, function (error) {
|
||||
if (error) $scope.mailRelay.error = error.message;
|
||||
else $scope.mailRelay.success = true;
|
||||
|
||||
$scope.mailRelay.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.mailRelay.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.currentRelay = data;
|
||||
$scope.mailRelay.success = true;
|
||||
$scope.email.refresh();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sendTestEmail = function () {
|
||||
Client.sentTestMail($scope.user.email, function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.testEmail = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
$('#testEmailSent').modal('show');
|
||||
});
|
||||
mailTo: '',
|
||||
|
||||
clearForm: function () {
|
||||
$scope.testEmail.mailTo = '';
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
$scope.testEmail.mailTo = $scope.user.email;
|
||||
|
||||
$('#testEmailModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = true;
|
||||
|
||||
Client.sentTestMail($scope.testEmail.mailTo, function (error) {
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.testEmail.error.generic = error.message;
|
||||
console.error(error);
|
||||
$('#inputTestMailTo').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#testEmailModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getMailConfig() {
|
||||
@@ -185,6 +230,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.mailRelay.relay.password = '';
|
||||
$scope.mailRelay.relay.serverApiToken = '';
|
||||
|
||||
$scope.currentRelay = relay;
|
||||
|
||||
if (relay.provider === 'postmark-smtp') {
|
||||
$scope.mailRelay.relay.serverApiToken = relay.username;
|
||||
} else if (relay.provider === 'sendgrid-smtp') {
|
||||
@@ -203,33 +250,50 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
});
|
||||
}
|
||||
|
||||
// TODO this currently assumes the config.fqdn is the mail domain
|
||||
function getDnsConfig() {
|
||||
Client.getDnsConfig(function (error, dnsConfig) {
|
||||
Client.getDomain($scope.config.fqdn, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dnsConfig = dnsConfig;
|
||||
$scope.dnsConfig = result.config;
|
||||
});
|
||||
}
|
||||
|
||||
function collapseDnsRecords() {
|
||||
$scope.expectedDnsRecordsTypes.forEach(function (record) {
|
||||
var type = record.value;
|
||||
$('#collapse_dns_' + type).collapse('hide');
|
||||
});
|
||||
|
||||
$('#collapse_outbound_smtp').collapse('hide');
|
||||
$('#collapse_rbl').collapse('hide');
|
||||
}
|
||||
|
||||
function showExpectedDnsRecords(callback) {
|
||||
callback = callback || function (error) { if (error) console.error(error); };
|
||||
|
||||
Client.getEmailStatus(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.expectedDnsRecords = result.dns;
|
||||
$scope.relay = result.relay;
|
||||
$scope.rbl = result.rbl;
|
||||
|
||||
// open the record details if they are not correct
|
||||
for (var type in $scope.expectedDnsRecords) {
|
||||
$scope.expectedDnsRecordsTypes.forEach(function (record) {
|
||||
var type = record.value;
|
||||
$scope.expectedDnsRecords[type] = result.dns[type] || {};
|
||||
|
||||
if (!$scope.expectedDnsRecords[type].status) {
|
||||
$('#collapse_dns_' + type).collapse('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!$scope.relay.status) {
|
||||
$('#collapse_dns_port').collapse('show');
|
||||
$('#collapse_outbound_smtp').collapse('show');
|
||||
}
|
||||
|
||||
if (!$scope.rbl.status) {
|
||||
$('#collapse_rbl').collapse('show');
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -285,5 +349,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.email.refresh();
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['testEmailModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<fieldset>
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!planChangeForm.password.$dirty && planChange.error.password) || (planChangeForm.password.$dirty && planChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputDeveloperModeChangePassword">Give your password to verify that you are performing that action</label>
|
||||
<label class="control-label">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!planChangeForm.password.$dirty && planChange.error.password) || (planChangeForm.password.$dirty && planChangeForm.password.$invalid)">
|
||||
<small ng-show=" planChangeForm.password.$dirty && planChangeForm.password.$invalid">A password is required</small>
|
||||
<small ng-show="!planChangeForm.password.$dirty && planChange.error.password">Wrong password</small>
|
||||
@@ -125,7 +125,7 @@
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/references/selfhosting/#backups" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=configureBackup.clearForm()></select>
|
||||
</div>
|
||||
|
||||
@@ -183,6 +183,11 @@
|
||||
<select class="form-control" name="region" id="inputConfigureBackupRegion" ng-model="configureBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
@@ -403,7 +408,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<div ng-show="createBackup.busy" class="progress progress-striped active animateMe">
|
||||
@@ -412,7 +417,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row">
|
||||
<div class="col-md-11" ng-show="createBackup.busy">
|
||||
<p class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ createBackup.detail || 'Syncing ...' }}
|
||||
@@ -420,14 +425,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="createBackup.busy">{{ createBackup.message }}</p>
|
||||
<p ng-hide="createBackup.busy">
|
||||
<div class="has-error" ng-show="createBackup.percent === 100 && createBackup.result">{{ createBackup.result }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<div class="col-md-6 text-right" ng-show="backupConfig.provider !== 'caas'">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="configureBackup.show()" ng-disabled="createBackup.busy">Configure</button>
|
||||
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.doCreateBackup()" ng-disabled="createBackup.busy" style="margin-right: 10px">Backup now</button>
|
||||
|
||||
@@ -40,10 +40,15 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
{ name: 'US West (Oregon)', value: 'us-west-2' },
|
||||
];
|
||||
|
||||
$scope.doSpacesRegions = [
|
||||
{ name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' },
|
||||
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' }
|
||||
];
|
||||
|
||||
$scope.storageProvider = [
|
||||
{ name: 'Amazon S3', value: 's3' },
|
||||
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
|
||||
{ name: 'Google Cloud Storage', value: 'gcs' },
|
||||
{ name: 'DigitalOcean Spaces NYC3', value: 'digitalocean-spaces' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
{ name: 'Minio', value: 'minio' },
|
||||
@@ -422,7 +427,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v2';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.endpoint = 'https://nyc3.digitaloceanspaces.com';
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
} else if (backupConfig.provider === 'gcs'){
|
||||
|
||||
@@ -31,7 +31,7 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
|
||||
|
||||
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) {
|
||||
if (error) {
|
||||
$scope.feedback.error = error;
|
||||
$scope.feedback.error = error.message;
|
||||
} else {
|
||||
$scope.feedback.success = true;
|
||||
resetFeedback();
|
||||
|
||||
Reference in New Issue
Block a user