Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37e3278f23 | |||
| 7cee40b491 | |||
| fae23bd4fc | |||
| 148a189bb2 | |||
| c3778f94c4 | |||
| b7fbffcb42 | |||
| 6259849958 | |||
| eb767bb3b1 | |||
| a6f01b2455 | |||
| 4fe055c3a8 | |||
| 79d9cce2e7 | |||
| 9fbfdd08d8 | |||
| 879569c661 | |||
| 5814793dc1 | |||
| 299e40c389 | |||
| 38860cd70c | |||
| c8fe2611ba | |||
| af9175b30c | |||
| 35453a0c2d | |||
| fd91bf0498 | |||
| 3b02ef5591 | |||
| 2966763e9e | |||
| 6d7759a1af | |||
| 70e7ca395d | |||
| 922c587ca9 | |||
| a555d70868 | |||
| 6f6907363e | |||
| 77d601f0cc | |||
| 8e99f67fb7 | |||
| 9d3fa94960 | |||
| b6739e9d77 | |||
| 33c1b4ae3b | |||
| 67c0a4f513 | |||
| ce1181531a | |||
| 54682a1370 | |||
| dc5342b9fc | |||
| 83bb7c475d | |||
| 638bdc902b | |||
| 874064de67 | |||
| 1f134ff070 | |||
| 2c334170bd | |||
| 35efdf6cbd | |||
| e02f3d7064 | |||
| a5e83a4d84 | |||
| e6ba2a6e7a | |||
| 79dd50910c | |||
| c4d267ecb1 | |||
| 2011dd9a83 | |||
| b07131cd0f | |||
| d3fe165e2c | |||
| bf19de3a90 | |||
| 58a0b3d8e7 | |||
| 65c2ee1760 | |||
| dfb0a7fee1 | |||
| 7511339656 | |||
| cb106f8a55 | |||
| 39d45b71d7 | |||
| db1fa84936 | |||
| f83295372b | |||
| e6506d9458 | |||
| af63dbb31d | |||
| b5641cc445 | |||
| 576fb392bb | |||
| ff539e2669 | |||
| 506d3adf70 | |||
| 94eb7849fe | |||
| 9036b272a8 | |||
| c81467da7c | |||
| 6db3a20021 | |||
| a428d6c553 | |||
| b7b01d5605 | |||
| 500d2361ec | |||
| 75ba20201e | |||
| b26c8d20cd | |||
| 951ed4bf33 | |||
| 2a05ec3866 | |||
| 04f2bd1ec3 | |||
| e08116c9ad | |||
| da7fbeee3d | |||
| 61aa32d8c5 | |||
| 74ff5e8de4 | |||
| aad70a49b7 | |||
| d332bb05fa | |||
| 6b6781eabb | |||
| 4a1cdd4ef1 | |||
| 764a8f6a85 | |||
| 22a0b84c2a | |||
| bba911165b | |||
| 8656bea4f2 | |||
| 9024844449 | |||
| 89c5b81eb0 | |||
| 18a7b0e615 | |||
| 1407fbeb8c | |||
| b5fc377dab | |||
| 71af16beb9 | |||
| 96d3eda02b | |||
| ba2a6bab68 | |||
| 092cc40da6 | |||
| c55152c0e1 | |||
| e83bb0c639 | |||
| 318285cb07 | |||
| 5274e1c454 | |||
| 294a535c1b | |||
| eaeb80e3c0 | |||
| 6eb8047686 | |||
| db040bf293 | |||
| acfc1ede6e | |||
| 8910c76bcf | |||
| 342093f661 | |||
| 9e26db3cd2 | |||
| a71b39ddee | |||
| 0626354844 | |||
| e9d2a53aaf | |||
| ca59bbe1aa | |||
| f505b1a553 | |||
| a237b11ff7 | |||
| 9a77f012d8 | |||
| 36c7f779f3 | |||
| b970e90178 | |||
| a7ea34914d | |||
| 19e1e5861b | |||
| e23777a642 | |||
| a2f47f3ee2 | |||
| 15e0f11bb9 | |||
| 1a32ea511e | |||
| ac602dc2a9 | |||
| cf3fc940d2 | |||
| e09cac4ea1 | |||
| 7c96115ea9 | |||
| 12de353427 | |||
| 057e4db6c1 | |||
| 883915c9d3 | |||
| 898413bfd4 | |||
| aa02d839a7 | |||
| a4ba3a4dd0 |
@@ -2628,7 +2628,12 @@
|
||||
* Fix ipv4 vs ipv6 detection
|
||||
* Fix misleading pending security updates message
|
||||
|
||||
[7.4.3]
|
||||
* **IMPORTANT**: This is the last release of Cloudron to support Ubuntu 18.04. Please [upgrade](https://docs.cloudron.io/guides/upgrade-ubuntu-20/) to Ubuntu 20.04 (Focal Fossa).
|
||||
* postgresql: fix for supporting Taiga with postgres 14
|
||||
[7.5.0]
|
||||
* acme: handle LE validation type cache logic
|
||||
* improve viewing of logs
|
||||
* redis: update to 7.0.11
|
||||
* ionos profitbricks: add new regions Berlin and Logrono
|
||||
* docker: update to 23.0.6
|
||||
* network: trusted IPs
|
||||
* fix crash when editing quota of new mailboxes
|
||||
|
||||
|
||||
@@ -79,8 +79,6 @@ async function main() {
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
|
||||
|
||||
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const database = require('./src/database.js');
|
||||
|
||||
const crashNotifier = require('./src/crashnotifier.js');
|
||||
|
||||
// This is triggered by systemd with the crashed unit name as argument
|
||||
async function main() {
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
|
||||
|
||||
const unitName = process.argv[2];
|
||||
console.log('Started crash notifier for', unitName);
|
||||
|
||||
// eventlog api needs the db
|
||||
await database.initialize();
|
||||
|
||||
await crashNotifier.sendFailureLogs(unitName);
|
||||
}
|
||||
|
||||
main();
|
||||
+9
-10
@@ -2,15 +2,15 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var argv = require('yargs').argv,
|
||||
const argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
ejs = require('gulp-ejs'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
gulp = require('gulp'),
|
||||
rimraf = require('rimraf'),
|
||||
sass = require('gulp-sass')(require('node-sass')),
|
||||
sass = require('gulp-sass')(require('sass')),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
@@ -142,11 +142,11 @@ gulp.task('js-terminal', function () {
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-login', function () {
|
||||
return gulp.src(['src/js/login.js', 'src/js/utils.js'])
|
||||
gulp.task('js-passwordreset', function () {
|
||||
return gulp.src(['src/js/passwordreset.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('login.js', { newLine: ';' }))
|
||||
.pipe(concat('passwordreset.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
@@ -187,7 +187,7 @@ gulp.task('js-restore', function () {
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-login', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-passwordreset', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
@@ -245,8 +245,7 @@ gulp.task('timezones', function (done) {
|
||||
// --------------
|
||||
|
||||
gulp.task('clean', function (done) {
|
||||
rimraf.sync('dist');
|
||||
done();
|
||||
fs.rm('dist', { recursive: true, force: true }, done);
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'translation', 'images', 'css']));
|
||||
@@ -266,7 +265,7 @@ gulp.task('watch', function (done) {
|
||||
gulp.watch(['src/js/logs.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-logs']));
|
||||
gulp.watch(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js', 'src/components/*.js'], gulp.series(['js-filemanager']));
|
||||
gulp.watch(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-terminal']));
|
||||
gulp.watch(['src/js/login.js', 'src/js/utils.js'], gulp.series(['js-login']));
|
||||
gulp.watch(['src/js/passwordreset.js', 'src/js/utils.js'], gulp.series(['js-passwordreset']));
|
||||
gulp.watch(['src/js/setupaccount.js', 'src/js/utils.js'], gulp.series(['js-setupaccount']));
|
||||
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/main.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index']));
|
||||
gulp.watch(['src/3rdparty/**/*'], gulp.series(['3rdparty']));
|
||||
|
||||
Generated
+343
-3762
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,9 @@
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"bootstrap-sass": "^3.4.1",
|
||||
"chart.js": "^4.1.1",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"bootstrap-sass": "^3.4.3",
|
||||
"chart.js": "^4.3.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
@@ -25,13 +25,12 @@
|
||||
"gulp-serve": "^1.4.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"node-sass": "^7.0.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"xterm": "^5.1.0",
|
||||
"monaco-editor": "^0.39.0",
|
||||
"sass": "^1.63.3",
|
||||
"xterm": "^5.2.1",
|
||||
"xterm-addon-attach": "^0.8.0",
|
||||
"xterm-addon-fit": "^0.7.0",
|
||||
"yargs": "^17.5.1"
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Custom library to add password show/hide icons to input element with `password-reveal` attribute
|
||||
// util.js has the angular version, this is for plain js
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
|
||||
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
|
||||
|
||||
document.querySelectorAll('[password-reveal]').forEach(function (element) {
|
||||
var eye = document.createElement('i');
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
eye.style.width = '18px';
|
||||
eye.style.height = '18px';
|
||||
eye.style.position = 'relative';
|
||||
eye.style.float = 'right';
|
||||
eye.style.marginTop = '-24px';
|
||||
eye.style.marginRight = '10px';
|
||||
eye.style.cursor = 'pointer';
|
||||
|
||||
eye.addEventListener('click', function () {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
eye.innerHTML = svgEye;
|
||||
} else {
|
||||
element.type = 'password';
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
}
|
||||
});
|
||||
|
||||
element.parentNode.style.position = 'relative';
|
||||
element.parentNode.insertBefore(eye, element.nextSibling);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
|
||||
var tmp = window.location.hash.slice(1).split('&');
|
||||
|
||||
tmp.forEach(function (pair) {
|
||||
if (pair.indexOf('access_token=') === 0) localStorage.token = pair.split('=')[1];
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
|
||||
</script>
|
||||
@@ -159,7 +159,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-th fa-fw"></i> {{ 'apps.title' | tr }}</a>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-grip fa-fw"></i> {{ 'apps.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ 'appstore.title' | tr }}</a>
|
||||
|
||||
+59
-20
@@ -185,9 +185,11 @@ const REGIONS_OVH = [
|
||||
{ name: 'Warsaw (WAW)', value: 'https://s3.waw.cloud.ovh.net', region: 'waw' },
|
||||
];
|
||||
|
||||
// https://devops.ionos.com/api/s3/
|
||||
// https://docs.ionos.com/cloud/managed-services/s3-object-storage/endpoints
|
||||
const REGIONS_IONOS = [
|
||||
{ name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
{ name: 'Frankfurt (DE)', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
{ name: 'Berlin (eu-central-2)', value: 'https://s3-eu-central-2.ionoscloud.com', region: 'eu-central-2' }, // default
|
||||
{ name: 'Logrono (eu-south-2)', value: 'https://s3-eu-south-2.ionoscloud.com', region: 'eu-south-2' }, // default
|
||||
];
|
||||
|
||||
// this is not used anywhere because upcloud needs endpoint URL. we detect region from the URL (https://upcloud.com/data-centres)
|
||||
@@ -1008,6 +1010,14 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getBackupMountStatus = function (callback) {
|
||||
get('/api/v1/backups/mount_status', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
Client.prototype.remountBackupStorage = function (callback) {
|
||||
post('/api/v1/backups/remount', {}, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1118,9 +1128,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
};
|
||||
|
||||
Client.prototype.getBlocklist = function (callback) {
|
||||
var config = {};
|
||||
|
||||
get('/api/v1/network/blocklist', config, function (error, data, status) {
|
||||
get('/api/v1/network/blocklist', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null, data.blocklist);
|
||||
@@ -1136,6 +1144,23 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getTrustedIps = function (callback) {
|
||||
get('/api/v1/network/trusted_ips', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null, data.trustedIps);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setTrustedIps = function (trustedIps, callback) {
|
||||
post('/api/v1/network/trusted_ips', { trustedIps: trustedIps }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setDynamicDnsConfig = function (enabled, callback) {
|
||||
post('/api/v1/settings/dynamic_dns', { enabled: enabled }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1361,6 +1386,15 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getTasksByType = function (type, callback) {
|
||||
get('/api/v1/tasks?type=' + type, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data.tasks);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getTask = function (taskId, callback) {
|
||||
get('/api/v1/tasks/' + taskId, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1373,7 +1407,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.getTaskLogs = function (taskId, follow, lines, callback) {
|
||||
if (follow) {
|
||||
var eventSource = new EventSource(client.apiOrigin + '/api/v1/tasks/' + taskId + '/logstream?lines=' + lines + '&access_token=' + token);
|
||||
callback(null, eventSource);
|
||||
eventSource.onerror = callback;
|
||||
eventSource.onopen = function () { callback(null, eventSource); };
|
||||
} else {
|
||||
get('/api/v1/services/' + taskId + '/logs?lines=' + lines, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1510,7 +1545,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.getPlatformLogs = function (unit, follow, lines, callback) {
|
||||
if (follow) {
|
||||
var eventSource = new EventSource(client.apiOrigin + '/api/v1/cloudron/logstream/' + unit + '?lines=' + lines + '&access_token=' + token);
|
||||
callback(null, eventSource);
|
||||
eventSource.onerror = callback;
|
||||
eventSource.onopen = function () { callback(null, eventSource); };
|
||||
} else {
|
||||
get('/api/v1/cloudron/logs/' + unit + '?lines=' + lines, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1524,7 +1560,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.getServiceLogs = function (serviceName, follow, lines, callback) {
|
||||
if (follow) {
|
||||
var eventSource = new EventSource(client.apiOrigin + '/api/v1/services/' + serviceName + '/logstream?lines=' + lines + '&access_token=' + token);
|
||||
callback(null, eventSource);
|
||||
eventSource.onerror = callback;
|
||||
eventSource.onopen = function () { callback(null, eventSource); };
|
||||
} else {
|
||||
get('/api/v1/services/' + serviceName + '/logs?lines=' + lines, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1554,7 +1591,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.getAppLogs = function (appId, follow, lines, callback) {
|
||||
if (follow) {
|
||||
var eventSource = new EventSource(client.apiOrigin + '/api/v1/apps/' + appId + '/logstream?lines=' + lines + '&access_token=' + token);
|
||||
callback(null, eventSource);
|
||||
eventSource.onerror = callback;
|
||||
eventSource.onopen = function () { callback(null, eventSource); };
|
||||
} else {
|
||||
get('/api/v1/apps/' + appId + '/logs', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -2624,7 +2662,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
apps = apps.concat(applinks);
|
||||
|
||||
async.eachLimit(apps, 20, function (app, iteratorCallback) {
|
||||
app.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oidc'] || app.manifest.addons['proxyAuth']) && app.sso;
|
||||
app.ssoAuth = app.sso && (app.manifest.addons['ldap'] || app.manifest.addons['oidc'] || app.manifest.addons['proxyAuth']); // checking app.sso first ensures app.manifest.addons is not null
|
||||
|
||||
if (app.accessLevel !== 'operator' && app.accessLevel !== 'admin') { // only fetch if we have permissions
|
||||
app.progress = 0;
|
||||
@@ -2675,15 +2713,21 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.login = function () {
|
||||
this.setToken(null);
|
||||
|
||||
window.location.href = '/login.html?returnTo=/' + encodeURIComponent(window.location.hash);
|
||||
// start oidc flow
|
||||
window.location.href = this.apiOrigin + '/openid/auth?client_id=' + ('<%= apiOrigin %>' ? 'development' : 'dashboard') + '&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
};
|
||||
|
||||
Client.prototype.logout = function () {
|
||||
var token = this.getToken();
|
||||
this.setToken(null);
|
||||
var that = this;
|
||||
|
||||
// invalidates the token
|
||||
window.location.href = client.apiOrigin + '/api/v1/cloudron/logout?access_token=' + token;
|
||||
// destroy oidc session in the spirit of true SSO
|
||||
del('/api/v1/oidc/sessions', null, function (error, data, status) {
|
||||
if (error) console.error('Failed to logout from oidc session');
|
||||
|
||||
that.setToken(null);
|
||||
|
||||
window.location.href = '/';
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getAppEventLog = function (appId, page, perPage, callback) {
|
||||
@@ -3737,8 +3781,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
|
||||
var ACTION_DYNDNS_UPDATE = 'dyndns.update';
|
||||
|
||||
var ACTION_SYSTEM_CRASH = 'system.crash';
|
||||
|
||||
var data = eventLog.data;
|
||||
var errorMessage = data.errorMessage;
|
||||
var details, app;
|
||||
@@ -4053,9 +4095,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
case ACTION_SUPPORT_TICKET:
|
||||
return 'Support ticket was created';
|
||||
|
||||
case ACTION_SYSTEM_CRASH:
|
||||
return 'A system process crashed';
|
||||
|
||||
case ACTION_VOLUME_ADD:
|
||||
return 'Volume "' + data.volume.name + '" was added';
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ app.controller('LogsController', ['$scope', '$translate', 'Client', function ($s
|
||||
$scope.lines = 100;
|
||||
$scope.selectedAppInfo = null;
|
||||
$scope.selectedTaskInfo = null;
|
||||
$scope.error = null;
|
||||
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
@@ -54,8 +55,11 @@ app.controller('LogsController', ['$scope', '$translate', 'Client', function ($s
|
||||
else if ($scope.selected.type === 'task') func = Client.getTaskLogs;
|
||||
else if ($scope.selected.type === 'app') func = Client.getAppLogs;
|
||||
|
||||
func($scope.selected.value, true /* follow */, $scope.lines, function handleLogs(error, result) {
|
||||
if (error) return console.error(error);
|
||||
func($scope.selected.value, true /* follow */, $scope.lines, function (error, result) {
|
||||
if (error) {
|
||||
$scope.$apply(function () { $scope.error = { logsGone: true }; });
|
||||
return console.error('Error subscribing to logstream.', error);
|
||||
}
|
||||
|
||||
$scope.activeEventSource = result;
|
||||
result.onmessage = function handleMessage(message) {
|
||||
@@ -180,13 +184,16 @@ app.controller('LogsController', ['$scope', '$translate', 'Client', function ($s
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
select({ id: search.id, taskId: search.taskId, appId: search.appId, crashId: search.crashId }, function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
$scope.initialized = true;
|
||||
|
||||
if (error) {
|
||||
$scope.error = { notFound: true };
|
||||
return console.error('Not found.', error);
|
||||
}
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
showLogs();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
|
||||
app.controller('LoginController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
app.controller('PasswordResetController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
@@ -74,50 +74,6 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
|
||||
$scope.newPasswordRepeat = '';
|
||||
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
|
||||
|
||||
$scope.onLogin = function () {
|
||||
$scope.busy = true;
|
||||
$scope.error = false;
|
||||
|
||||
var data = {
|
||||
username: $scope.username,
|
||||
password: $scope.password,
|
||||
totpToken: $scope.totpToken
|
||||
};
|
||||
|
||||
function error(data, status) {
|
||||
$scope.busy = false;
|
||||
$scope.error = {};
|
||||
|
||||
if (!data || status !== 401) return $scope.error.internal = true;
|
||||
|
||||
if (data.message === 'Username and password does not match') {
|
||||
$scope.error.password = true;
|
||||
$scope.password = '';
|
||||
setTimeout(function () { $('#inputPassword').focus(); }, 200);
|
||||
} else if (data.message.indexOf('totpToken') !== -1) {
|
||||
$scope.error.totpToken = true;
|
||||
$scope.totpToken = '';
|
||||
setTimeout(function () { $('#inputTotpToken').focus(); }, 200);
|
||||
} else {
|
||||
$scope.error.generic = true;
|
||||
}
|
||||
|
||||
$scope.loginForm.$setPristine();
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/cloudron/login', data).success(function (data, status) {
|
||||
if (status !== 200) return error(data, status);
|
||||
|
||||
localStorage.token = data.accessToken;
|
||||
|
||||
// prevent redirecting to random domains
|
||||
var returnTo = search.returnTo || '/';
|
||||
if (returnTo.indexOf('/') !== 0) returnTo = '/';
|
||||
|
||||
window.location.href = returnTo;
|
||||
}).error(error);
|
||||
};
|
||||
|
||||
$scope.onPasswordReset = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
@@ -170,13 +126,6 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
|
||||
setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200);
|
||||
};
|
||||
|
||||
$scope.showLogin = function () {
|
||||
if ($scope.status) window.document.title = $scope.status.cloudronName + ' Login';
|
||||
$scope.mode = 'login';
|
||||
$scope.error = false;
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
};
|
||||
|
||||
$scope.showNewPassword = function () {
|
||||
window.document.title = 'Set New Password';
|
||||
$scope.mode = 'newPassword';
|
||||
@@ -190,7 +139,6 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
if ($scope.mode === 'login') window.document.title = data.cloudronName + ' Login';
|
||||
$scope.status = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
@@ -205,6 +153,6 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
$scope.showLogin();
|
||||
$scope.showPasswordReset();
|
||||
}
|
||||
}]);
|
||||
@@ -280,7 +280,7 @@ angular.module('Application').controller('TerminalController', ['$scope', '$tran
|
||||
$scope.terminal.focus();
|
||||
};
|
||||
|
||||
$scope.terminalCopy = function () {
|
||||
$scope.terminalCopyToClipboard = function () {
|
||||
if (!$scope.terminal) return;
|
||||
|
||||
// execCommand('copy') would copy any selection from the page, so do this only if terminal has a selection
|
||||
@@ -363,6 +363,13 @@ angular.module('Application').controller('TerminalController', ['$scope', '$tran
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'C' && event.ctrlKey) { // ctrl shift c
|
||||
event.preventDefault();
|
||||
$scope.terminalCopyToClipboard();
|
||||
}
|
||||
});
|
||||
|
||||
$translate([ 'terminal.title' ]).then(function (tr) {
|
||||
if (tr['terminal.title'] !== 'terminal.title') window.document.title = tr['terminal.title'];
|
||||
});
|
||||
|
||||
@@ -65,13 +65,15 @@
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<div class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
<div class="logs-controls">
|
||||
<div ng-show="error.notFound" class="logs-error">{{ 'logs.notFoundError' | tr }}</div>
|
||||
<div ng-show="error.logsGone" class="logs-error">{{ 'logs.logsGoneError' | tr }}</div>
|
||||
<div ng-hide="error" class="logs-controls">
|
||||
<h3 style="display: inline-block;">{{ selected.name }}</h3>
|
||||
|
||||
<!-- logs actions -->
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-primary" ng-href="{{ '/terminal.html?id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fa fa-terminal"></i> {{ 'terminal.title' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-href="{{ '/filemanager.html?type=app&id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fas fa-folder"></i> {{ 'filemanager.title' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-href="{{ '/filemanager/#/home/app/' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fas fa-folder"></i> {{ 'filemanager.title' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-click="clear()"><i class="fa fa-trash"></i> {{ 'logs.clear' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=-1"><i class="fa fa-download"></i> {{ 'logs.download' | tr }}</a>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
|
||||
|
||||
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>Cloudron Login</title>
|
||||
<meta name="description" content="Cloudron Login">
|
||||
<title>Cloudron Password Reset</title>
|
||||
<meta name="description" content="Cloudron Password Reset">
|
||||
|
||||
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
@@ -47,54 +47,14 @@
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/login.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/js/passwordreset.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="LoginController">
|
||||
<body ng-app="Application" ng-controller="PasswordResetController">
|
||||
|
||||
<div class="layout-root ng-cloak" ng-show="initialized">
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'login'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h1><small>{{ 'login.loginTo' | tr }}</small> {{ status.cloudronName || 'Cloudron' }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" ng-show="error && (error.generic || error.password)">{{ 'login.errorIncorrectCredentials' | tr }}</h4>
|
||||
<h4 class="has-error" ng-show="error && error.totpToken">{{ 'login.errorIncorrect2FAToken' | tr }}</h4>
|
||||
<h4 class="has-error" ng-show="error && error.internal">{{ 'login.errorInternal' | tr }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="loginForm" ng-submit="onLogin()">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">{{ 'login.username' | tr }}</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" ng-model="username" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': error.password }">
|
||||
<label class="control-label" for="inputPassword">{{ 'login.password' | tr }}</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" ng-model="password" ng-disabled="busy" required password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': error.totpToken }">
|
||||
<label class="control-label" for="inputTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || loginForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'login.signInAction' | tr }}</button>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showPasswordReset()">{{ 'login.resetPasswordAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'passwordReset'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
@@ -113,9 +73,11 @@
|
||||
<input type="text" class="form-control" id="inputPasswordResetIdentifier" name="passwordResetIdentifier" ng-model="passwordResetIdentifier" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<br/>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
|
||||
</div>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,7 +91,7 @@
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.emailSent.title' | tr }}</h2>
|
||||
<br/>
|
||||
<button class="btn btn-primary" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</button>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,10 +135,11 @@
|
||||
<label class="control-label" for="inputPasswordResetTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="passwordResetTotpToken" id="inputPasswordResetTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<br/>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
|
||||
</div>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
<div class="contextMenuBackdrop">
|
||||
<ul class="dropdown-menu" id="terminalContextMenu" style="position: absolute; display:none;">
|
||||
<li><a href="" ng-click="terminalCopy()">{{ 'terminal.contextmenu.copy' | tr }}</a></li>
|
||||
<li><a href="" ng-click="terminalCopyToClipboard()">{{ 'terminal.contextmenu.copy' | tr }}</a></li>
|
||||
<li class="disabled"><a>{{ 'terminal.contextmenu.pasteInfo' | tr }}</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="" ng-click="terminalClear()">{{ 'terminal.contextmenu.clear' | tr }}</a></li>
|
||||
|
||||
@@ -320,6 +320,10 @@ h1, h2, h3 {
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.offscreen {
|
||||
position: absolute;
|
||||
left: -999em;
|
||||
@@ -824,6 +828,19 @@ multiselect {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Login and password forms
|
||||
// ----------------------------
|
||||
|
||||
.card-form-bottom-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-form-bottom-bar > * {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Appstore view
|
||||
// ----------------------------
|
||||
@@ -1757,6 +1774,14 @@ tag-input {
|
||||
.logs {
|
||||
background: black;
|
||||
|
||||
.logs-error {
|
||||
color: white;
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
margin-top: 200px;
|
||||
}
|
||||
|
||||
.logs-controls {
|
||||
margin: 5px;
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"save": "Gem",
|
||||
"close": "Luk",
|
||||
"no": "Nej",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"delete": "Slet"
|
||||
},
|
||||
"username": "Brugernavn",
|
||||
"displayName": "Vis navn",
|
||||
@@ -87,7 +88,8 @@
|
||||
"statusEnabled": "Aktiveret",
|
||||
"statusDisabled": "Slået fra",
|
||||
"loadingPlaceholder": "Indlæsning",
|
||||
"disableAction": "Deaktiver"
|
||||
"disableAction": "Deaktiver",
|
||||
"settings": "Indstillinger"
|
||||
},
|
||||
"appstore": {
|
||||
"category": {
|
||||
@@ -1013,7 +1015,8 @@
|
||||
"hetznerToken": "Hetzner Token",
|
||||
"porkbunSecretapikey": "Hemmelig API-nøgle",
|
||||
"cloudflareDefaultProxyStatus": "Aktiver proxying for nye DNS-poster",
|
||||
"porkbunApikey": "API-nøgle"
|
||||
"porkbunApikey": "API-nøgle",
|
||||
"bunnyAccessKey": "Bunny Access Key"
|
||||
},
|
||||
"title": "Domæner og certs",
|
||||
"addDomain": "Tilføj domæne",
|
||||
@@ -1042,7 +1045,8 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Well-Known locations på {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Indstil well-known lokationer"
|
||||
"tooltipWellKnown": "Indstil well-known lokationer",
|
||||
"count": "Samlede domæner: {{ count }}"
|
||||
},
|
||||
"notifications": {
|
||||
"markAllAsRead": "Markér alle som læst",
|
||||
@@ -1817,7 +1821,9 @@
|
||||
"password": "Adgangskode",
|
||||
"2faToken": "2FA-token (hvis aktiveret)",
|
||||
"signInAction": "Log ind",
|
||||
"resetPasswordAction": "Nulstil adgangskode"
|
||||
"resetPasswordAction": "Nulstil adgangskode",
|
||||
"errorIncorrect2FAToken": "2FA-token er ugyldig",
|
||||
"errorInternal": "Intern fejl, prøv igen senere"
|
||||
},
|
||||
"lang": {
|
||||
"en": "English",
|
||||
@@ -1831,9 +1837,47 @@
|
||||
"zh_Hans": "Kinesisk (forenklet)",
|
||||
"es": "Spansk",
|
||||
"ru": "Russisk",
|
||||
"pt": "Portugisisk"
|
||||
"pt": "Portugisisk",
|
||||
"da": "Dansk"
|
||||
},
|
||||
"supportConfig": {
|
||||
"emailNotVerified": "Du bedes først bekræfte e-mailen på cloudron.io-kontoen for at sikre, at vi kan kontakte dig."
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Tilføj klient",
|
||||
"description": "Tilføj nye OpenID connect-klientindstillinger.",
|
||||
"createAction": "Opret"
|
||||
},
|
||||
"client": {
|
||||
"name": "Navn",
|
||||
"id": "Klient-id",
|
||||
"secret": "Klientens secret",
|
||||
"signingAlgorithm": "Signeringsalgoritme",
|
||||
"loginRedirectUri": "Url til tilbagekaldelse af login (kommasepareret, hvis der er mere end én)",
|
||||
"logoutRedirectUri": "Url til tilbagekaldelse af logout (valgfrit)"
|
||||
},
|
||||
"title": "OpenID Connect-udbyder",
|
||||
"description": "Cloudron kan fungere som OpenID Connect-udbyder for interne apps og eksterne tjenester.",
|
||||
"editClientDialog": {
|
||||
"title": "Rediger klient {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Virkelig slette klient {{ client }}?",
|
||||
"description": "Dette vil afbryde forbindelsen til alle eksterne OpenID-apps fra denne Cloudron, der bruger dette klient-id."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL til opdagelse",
|
||||
"logoutUrl": "URL til logout",
|
||||
"profileEndpoint": "Profil slutpunkt",
|
||||
"keysEndpoint": "Nøgler Slutpunkt",
|
||||
"tokenEndpoint": "Token slutpunkt",
|
||||
"authEndpoint": "Auth-slutpunkt"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Klienter",
|
||||
"newClient": "Ny klient",
|
||||
"empty": "Ingen klienten endnu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1366,7 +1366,8 @@
|
||||
"paste": "Einfügen",
|
||||
"copy": "Kopieren",
|
||||
"cut": "Ausschneiden",
|
||||
"edit": "Bearbeiten"
|
||||
"edit": "Bearbeiten",
|
||||
"open": "Öffnen"
|
||||
},
|
||||
"symlink": "Symlink zu {{ target }}",
|
||||
"mtime": "Geändert"
|
||||
|
||||
@@ -806,7 +806,13 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Configure IPv6 Provider"
|
||||
}
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "HTTP headers from matching IP addresses will be trusted",
|
||||
"title": "Configure Trusted IPs",
|
||||
"summary": "{{ trustCount }} IPs trusted"
|
||||
},
|
||||
"trustedIpRanges": "Trusted IPs & Ranges "
|
||||
},
|
||||
"services": {
|
||||
"title": "Services",
|
||||
@@ -1066,7 +1072,9 @@
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
"clear": "Clear View",
|
||||
"download": "Download Full Logs"
|
||||
"download": "Download Full Logs",
|
||||
"notFoundError": "No such task or app",
|
||||
"logsGoneError": "Log file(s) not found"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -1164,7 +1172,8 @@
|
||||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"selectAll": "Select All"
|
||||
"selectAll": "Select All",
|
||||
"open": "Open"
|
||||
},
|
||||
"mtime": "Modified"
|
||||
},
|
||||
@@ -1179,7 +1188,17 @@
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "restarting app"
|
||||
}
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Uploading",
|
||||
"exitWarning": "Upload still in progress. Really close this page?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"save": "Save"
|
||||
},
|
||||
"extractionInProgress": "Extraction in progress"
|
||||
},
|
||||
"email": {
|
||||
"backAction": "Back to Email",
|
||||
@@ -1879,5 +1898,6 @@
|
||||
"newClient": "New client",
|
||||
"empty": "No clients yet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automation": "Automation"
|
||||
}
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
"pagination": {
|
||||
"perPageSelector": "Mostrar {{ n }} por página",
|
||||
"next": "siguiente",
|
||||
"prev": "anterior"
|
||||
"prev": "anterior",
|
||||
"itemCount": "Encontrado {{ count }}"
|
||||
},
|
||||
"table": {
|
||||
"date": "Fecha"
|
||||
@@ -115,7 +116,8 @@
|
||||
"no": "No",
|
||||
"close": "Cerrar",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar"
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Borrar"
|
||||
},
|
||||
"logout": "Salir",
|
||||
"offline": "Cloudron está desconectado. Reconectando…",
|
||||
@@ -137,7 +139,9 @@
|
||||
},
|
||||
"enableAction": "Habilitar",
|
||||
"statusEnabled": "Habilitado",
|
||||
"statusDisabled": "Deshabilitado"
|
||||
"statusDisabled": "Deshabilitado",
|
||||
"loadingPlaceholder": "Cargando",
|
||||
"settings": "Ajustes"
|
||||
},
|
||||
"apps": {
|
||||
"domainsFilterHeader": "Todos los Dominios",
|
||||
@@ -950,7 +954,11 @@
|
||||
"vultrToken": "Token Vultr",
|
||||
"jitsiHostname": "Ubicación de Jitsi",
|
||||
"wellKnownDescription": "Cloudron utilizará los valores para responder a las URLs <code>/.well-known/</code> . Ten en cuenta que la aplicación debe estar disponible en el dominio desnudo <code>{{ domain }}</code> para que esto funcione. Consulta <a href=\"{{docsLink}}\" target=\"_blank\">esta documentación</a> para más información.",
|
||||
"hetznerToken": "Token de Hetzner"
|
||||
"hetznerToken": "Token de Hetzner",
|
||||
"bunnyAccessKey": "Clave de acceso Bunny",
|
||||
"cloudflareDefaultProxyStatus": "Habilitar proxy para nuevos registros DNS",
|
||||
"porkbunApikey": "Clave API",
|
||||
"porkbunSecretapikey": "Clave API secreta"
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"setupAction": "Configura tu suscripción",
|
||||
@@ -982,7 +990,8 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Ubicaciones Well-known de {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Establece las ubicaciones Well-Known"
|
||||
"tooltipWellKnown": "Establece las ubicaciones Well-Known",
|
||||
"count": "Dominios totales: {{ count }}"
|
||||
},
|
||||
"app": {
|
||||
"appInfo": {
|
||||
@@ -1098,7 +1107,8 @@
|
||||
"saveAction": "Guardar",
|
||||
"description": "La configuración de esta opción anulará cualquier encabezado CSP enviado por la propia aplicación",
|
||||
"title": "Política de seguridad de contenido"
|
||||
}
|
||||
},
|
||||
"hstsPreload": "Habilitar la carga previa de HSTS para este sitio y todos los subdominios"
|
||||
},
|
||||
"email": {
|
||||
"from": {
|
||||
@@ -1207,7 +1217,8 @@
|
||||
"description": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una importación.",
|
||||
"title": "Importar Backup",
|
||||
"uploadAction": "Subir Configuración de Backup",
|
||||
"importAction": "Importar"
|
||||
"importAction": "Importar",
|
||||
"remotePath": "Ruta del Backup"
|
||||
},
|
||||
"restoreDialog": {
|
||||
"warning": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una restauración.",
|
||||
@@ -1324,7 +1335,8 @@
|
||||
"en": "Inglés",
|
||||
"es": "Español",
|
||||
"ru": "Ruso",
|
||||
"pt": "Portugués"
|
||||
"pt": "Portugués",
|
||||
"da": "Danés"
|
||||
},
|
||||
"system": {
|
||||
"title": "Información del Sistema",
|
||||
@@ -1345,7 +1357,8 @@
|
||||
"title": "Uso del Disco",
|
||||
"usedInfo": "{{ used }} usados de {{ size }}",
|
||||
"uninstalledApp": "Aplicación desinstalada",
|
||||
"volumeContent": "Este disco es el volumen <code>{{ name }}</code>"
|
||||
"volumeContent": "Este disco es el volumen <code>{{ name }}</code>",
|
||||
"diskSpeed": "Velocidad: {{ speed }} MB/seg"
|
||||
},
|
||||
"selectPeriodLabel": "Seleccionar Periodo"
|
||||
},
|
||||
@@ -1403,7 +1416,7 @@
|
||||
"removeVolumeActionTooltip": "Borrar Volumen",
|
||||
"openFileManagerActionTooltip": "Abrir Gestor de Archivos",
|
||||
"name": "Nombre",
|
||||
"hostPath": "Punto de montaje",
|
||||
"hostPath": "Objetivo",
|
||||
"addVolumeAction": "Añade un Volumen",
|
||||
"title": "Volúmenes",
|
||||
"description": "Los volúmenes son sistemas de archivos locales o remotos. Se pueden usar como el almacenamiento de datos principal de una aplicación o como una ubicación de almacenamiento compartida entre aplicaciones.",
|
||||
@@ -1811,7 +1824,9 @@
|
||||
"password": "Contraseña",
|
||||
"2faToken": "Token 2FA (si está habilitado)",
|
||||
"signInAction": "Iniciar sesión",
|
||||
"resetPasswordAction": "Resetear contraseña"
|
||||
"resetPasswordAction": "Resetear contraseña",
|
||||
"errorIncorrect2FAToken": "El token 2FA es inválido",
|
||||
"errorInternal": "Error interno, prueba de nuevo más tarde"
|
||||
},
|
||||
"newLoginEmail": {
|
||||
"subject": "[<% = cloudron%>] Nuevo inicio de sesión en tu cuenta",
|
||||
@@ -1827,5 +1842,42 @@
|
||||
"mounts": {
|
||||
"description": "Las aplicaciones pueden acceder a <a href=\"/#/volumes\">volúmenes</a> montados a través del directorio <code>/media/{volume name}</code>. Estos datos no están incluidos en la copia de seguridad de la aplicación."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Añadir Cliente",
|
||||
"description": "Agrega una nueva configuración de cliente de conexión de OpenID.",
|
||||
"createAction": "Crear"
|
||||
},
|
||||
"client": {
|
||||
"name": "Nombre",
|
||||
"id": "ID de cliente",
|
||||
"secret": "Secreto de cliente",
|
||||
"signingAlgorithm": "Algoritmo de firma",
|
||||
"loginRedirectUri": "URL de devolución de llamada de inicio de sesión (separadas por comas si hay más de una)",
|
||||
"logoutRedirectUri": "URL de devolución de llamada de cierre de sesión (opcional)"
|
||||
},
|
||||
"title": "Proveedor de conexión OpenID",
|
||||
"description": "Cloudron puede actuar como proveedor de OpenID Connect para aplicaciones internas y servicios externos.",
|
||||
"editClientDialog": {
|
||||
"title": "Editar cliente {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "¿Realmente quieres borrar el cliente {{ client }}?",
|
||||
"description": "Esto desconectará todas las aplicaciones OpenID externas de este Cloudron que utilicen este ID de cliente."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL de descubrimiento",
|
||||
"logoutUrl": "URL de cierre de sesión",
|
||||
"profileEndpoint": "Punto final del perfil",
|
||||
"keysEndpoint": "Punto final de claves",
|
||||
"tokenEndpoint": "Punto final del Token",
|
||||
"authEndpoint": "Punto final de autenticación"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clientes",
|
||||
"newClient": "Nuevo cliente",
|
||||
"empty": "No hay clientes aún"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"pagination": {
|
||||
"prev": "préc.",
|
||||
"next": "suiv.",
|
||||
"perPageSelector": "Afficher {{ n }} par page"
|
||||
"perPageSelector": "Afficher {{ n }} par page",
|
||||
"itemCount": "Trouvé {{ count }}"
|
||||
},
|
||||
"action": {
|
||||
"logs": "Journaux",
|
||||
@@ -85,7 +86,8 @@
|
||||
"users": "Utilisateurs"
|
||||
},
|
||||
"disableAction": "Désactiver",
|
||||
"enableAction": "Activer"
|
||||
"enableAction": "Activer",
|
||||
"loadingPlaceholder": "Chargement"
|
||||
},
|
||||
"users": {
|
||||
"title": "Annuaire des utilisateurs",
|
||||
@@ -1739,7 +1741,9 @@
|
||||
"usageInfo": "{{ available | prettyDiskSize }}</b> sur <b>{{ size | prettyDiskSize }}</b> disponible(s)",
|
||||
"mountedAt": "{{ filesystem }} <small>monté sur</small> {{ mountpoint }}",
|
||||
"title": "Utilisation du disque",
|
||||
"usedInfo": "{{ used }} utilisé de {{ size }}"
|
||||
"usedInfo": "{{ used }} utilisé de {{ size }}",
|
||||
"uninstalledApp": "Désinstaller App",
|
||||
"diskSpeed": "Vitesse : {{ speed }} MB/sec"
|
||||
},
|
||||
"title": "Info système"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"save": "Opslaan",
|
||||
"close": "Sluiten",
|
||||
"no": "Nee",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"delete": "Verwijder"
|
||||
},
|
||||
"username": "Gebruikersnaam",
|
||||
"displayName": "Naam",
|
||||
@@ -87,7 +88,8 @@
|
||||
"enableAction": "Inschakelen",
|
||||
"statusEnabled": "Ingeschakeld",
|
||||
"statusDisabled": "Uitgeschakeld",
|
||||
"loadingPlaceholder": "Laden"
|
||||
"loadingPlaceholder": "Laden",
|
||||
"settings": "Instellingen"
|
||||
},
|
||||
"appstore": {
|
||||
"title": "App Store",
|
||||
@@ -852,7 +854,8 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Well-Known locaties van {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Well-Known Locaties instellen"
|
||||
"tooltipWellKnown": "Well-Known Locaties instellen",
|
||||
"count": "Totaal domeinen: {{ count }}"
|
||||
},
|
||||
"app": {
|
||||
"email": {
|
||||
@@ -1224,7 +1227,13 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Configureer IPv6 aanbieder"
|
||||
}
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "HTTP headers van bijbehorende IP adressen worden vertrouwd",
|
||||
"summary": "{{ trustCount }} IP’s vertrouwd",
|
||||
"title": "Configureer vertrouwde IP’s"
|
||||
},
|
||||
"trustedIpRanges": "Vertrouwde IP’s & bereiken "
|
||||
},
|
||||
"services": {
|
||||
"title": "Diensten",
|
||||
@@ -1393,7 +1402,9 @@
|
||||
"logs": {
|
||||
"title": "Logbestanden",
|
||||
"clear": "Leegmaken",
|
||||
"download": "Download volledige logbestanden"
|
||||
"download": "Download volledige logbestanden",
|
||||
"notFoundError": "Geen taak of app gevonden",
|
||||
"logsGoneError": "Log bestand(en) niet gevonden"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -1512,7 +1523,7 @@
|
||||
"backAction": "Terug naar e-mail",
|
||||
"config": {
|
||||
"title": "E-mailconfiguratie {{ domain }}",
|
||||
"clientConfiguration": "Configureren E-mail clients"
|
||||
"clientConfiguration": "Configureren E-mail programma's"
|
||||
},
|
||||
"incoming": {
|
||||
"disableAction": "Uitschakelen",
|
||||
@@ -1558,7 +1569,7 @@
|
||||
"incomingPasswordUsage": "Wachtwoord van de eigenaar van de mailbox",
|
||||
"enabled": "Cloudron e-mailserver is geconfigureerd voor inkomende e-mails voor dit domein.",
|
||||
"disabled": "Cloudron e-mailserver ontvangt geen inkomende e-mails voor dit domein.",
|
||||
"howToConnectDescription": "Gebruik onderstaande gegevens om e-mail clients in te stellen."
|
||||
"howToConnectDescription": "Gebruik onderstaande gegevens om e-mail programma's in te stellen."
|
||||
},
|
||||
"outbound": {
|
||||
"tabTitle": "Uitgaand",
|
||||
@@ -1685,7 +1696,7 @@
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Mailing-lijst is actief"
|
||||
},
|
||||
"howToConnectInfoModal": "Configureren e-mail clients",
|
||||
"howToConnectInfoModal": "Configureren e-mail programma's",
|
||||
"mailboxImportDialog": {
|
||||
"title": "Importeer Mailboxen",
|
||||
"description": "Upload een JSON of CSV bestand met een schema zoals beschreven in onze <a href=\"{{ docsLink }}\" target=\"_blank\">documentatie</a>.",
|
||||
@@ -1703,7 +1714,9 @@
|
||||
"password": "Wachtwoord",
|
||||
"resetPasswordAction": "Herstel wachtwoord",
|
||||
"2faToken": "2FA Token (indien ingeschakeld)",
|
||||
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord"
|
||||
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"errorIncorrect2FAToken": "2FA token is niet geldig",
|
||||
"errorInternal": "Interne fout, probeer later opnieuw"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Wachtwoord herstellen",
|
||||
@@ -1837,5 +1850,43 @@
|
||||
"mounts": {
|
||||
"description": "Apps kunnen toegang krijgen tot <a href=\"/#/volumes\">volumes</a> via <code>/media/{volume name}</code> directory. Deze data is niet opgenomen in de app backup."
|
||||
}
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Client toevoegen",
|
||||
"description": "Nieuwe OpenID Connect client instellingen toevoegen.",
|
||||
"createAction": "Aanmaken"
|
||||
},
|
||||
"client": {
|
||||
"name": "Naam",
|
||||
"id": "Client ID",
|
||||
"secret": "Client geheim",
|
||||
"signingAlgorithm": "Ondertekeningsalgoritme",
|
||||
"loginRedirectUri": "Login callback URL (met komma gescheiden indien meer dan één)",
|
||||
"logoutRedirectUri": "Logout callback URL (optioneel)"
|
||||
},
|
||||
"title": "OpenID Connect aanbieder",
|
||||
"description": "Cloudron kan als een OpenID Connect aanbieder voor interne apps en externe diensten fungeren.",
|
||||
"editClientDialog": {
|
||||
"title": "Bewerk Client {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Weet je zeker dat je Client {{ client }} wilt verwijderen?",
|
||||
"description": "Hiermee worden alle externe OpenID apps met dit Client ID losgekoppeld."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "Discovery URL",
|
||||
"logoutUrl": "Logout URL",
|
||||
"profileEndpoint": "Profiel Eindpunt",
|
||||
"keysEndpoint": "Sleutels Eindpunt",
|
||||
"tokenEndpoint": "Token Eindpunt",
|
||||
"authEndpoint": "Auth Eindpunt"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clients",
|
||||
"newClient": "Nieuwe Client",
|
||||
"empty": "Nog geen Clients"
|
||||
}
|
||||
},
|
||||
"automation": "Automatisering"
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"save": "Сохранить",
|
||||
"close": "Закрыть",
|
||||
"no": "Нет",
|
||||
"yes": "Да"
|
||||
"yes": "Да",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"username": "Имя пользователя",
|
||||
"displayName": "Отображаемое имя",
|
||||
@@ -87,7 +88,8 @@
|
||||
"enableAction": "Включить",
|
||||
"statusEnabled": "Включено",
|
||||
"statusDisabled": "Выключено",
|
||||
"loadingPlaceholder": "Загрузка"
|
||||
"loadingPlaceholder": "Загрузка",
|
||||
"settings": "Настройки"
|
||||
},
|
||||
"appstore": {
|
||||
"category": {
|
||||
@@ -1134,7 +1136,13 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Настройка IPv6"
|
||||
}
|
||||
},
|
||||
"trustedIps": {
|
||||
"summary": "{{ trustCount }} IP доверены",
|
||||
"title": "Настроить доверенные IP",
|
||||
"description": "HTTP заголовки от совпадающих IP адресов будут доверены"
|
||||
},
|
||||
"trustedIpRanges": "Доверенные IP и диапазоны "
|
||||
},
|
||||
"services": {
|
||||
"title": "Службы",
|
||||
@@ -1363,7 +1371,8 @@
|
||||
"hetznerToken": "Токен Hetzner",
|
||||
"cloudflareDefaultProxyStatus": "Активировать прокси для новых DNS записей",
|
||||
"porkbunApikey": "API Ключ",
|
||||
"porkbunSecretapikey": "Secret API Ключ"
|
||||
"porkbunSecretapikey": "Secret API Ключ",
|
||||
"bunnyAccessKey": "Ключ доступа Bunny"
|
||||
},
|
||||
"addDomain": "Добавить домен",
|
||||
"removeDialog": {
|
||||
@@ -1380,7 +1389,8 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Общеизвестные расположения {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Установить общеизвестные расположения"
|
||||
"tooltipWellKnown": "Установить общеизвестные расположения",
|
||||
"count": "Всего доменов: {{ count }}"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Уведомления",
|
||||
@@ -1392,7 +1402,9 @@
|
||||
"logs": {
|
||||
"title": "Логи",
|
||||
"clear": "Очистить обзор",
|
||||
"download": "Скачать полные логи"
|
||||
"download": "Скачать полные логи",
|
||||
"notFoundError": "Задача или приложение не существует",
|
||||
"logsGoneError": "Файл(ы) журнала не найден(ы)"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Терминал",
|
||||
@@ -1702,7 +1714,9 @@
|
||||
"loginTo": "Войти в",
|
||||
"username": "Имя пользователя",
|
||||
"2faToken": "2FA Токен (если включен)",
|
||||
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль"
|
||||
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль",
|
||||
"errorIncorrect2FAToken": "Неверный 2FA токен",
|
||||
"errorInternal": "Внутренняя ошибка, попробуйте позже"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Сброс пароля",
|
||||
@@ -1776,7 +1790,8 @@
|
||||
"zh_Hans": "Китайский (Упрощенный)",
|
||||
"es": "Испанский",
|
||||
"ru": "Русский",
|
||||
"pt": "Португальский"
|
||||
"pt": "Португальский",
|
||||
"da": "Датский"
|
||||
},
|
||||
"setupAccount": {
|
||||
"username": "Имя пользователя",
|
||||
@@ -1835,5 +1850,43 @@
|
||||
"mounts": {
|
||||
"description": "Приложения могут получить доступ к смонтированным <a href=\"/#/volumes\">томам</a> по пути <code>/media/{имя тома}</code>. Данные таких томов не будут включаться в резервные копии приложения."
|
||||
}
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"createAction": "Создать",
|
||||
"title": "Добавить клиента",
|
||||
"description": "Добавить настройки нового клиента OpenID connect."
|
||||
},
|
||||
"client": {
|
||||
"name": "Имя",
|
||||
"id": "ID Клиента",
|
||||
"secret": "Секрет",
|
||||
"signingAlgorithm": "Метод подписи",
|
||||
"loginRedirectUri": "URL обратного вызова (если больше одного, отделите их запятой)",
|
||||
"logoutRedirectUri": "URL обратного вызова для выхода из системы (необязательно)"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Клиенты",
|
||||
"newClient": "Новый клиент",
|
||||
"empty": "Клиенты не найдены"
|
||||
},
|
||||
"title": "Поставщик OpenID Сonnect",
|
||||
"description": "Cloudron может выступать в качестве поставщика OpenID connect для внутренних приложений и внешних сервисов.",
|
||||
"editClientDialog": {
|
||||
"title": "Редактировать клиента {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Вы точно хотите удалить клиента {{ client }}?",
|
||||
"description": "Это действие отключит все внешние OpenID приложения, использующие данный клиент ID, от Cloudron."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL обнаружения",
|
||||
"logoutUrl": "URL выхода из системы",
|
||||
"profileEndpoint": "Конечная точка профиля",
|
||||
"keysEndpoint": "Конечная точка ключей",
|
||||
"tokenEndpoint": "Конечная точка токена",
|
||||
"authEndpoint": "Конечная точка аутентификации"
|
||||
}
|
||||
},
|
||||
"automation": "Автоматизация"
|
||||
}
|
||||
|
||||
@@ -583,7 +583,7 @@
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a class="btn btn-sm btn-default" ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-if="app.type !== APP_TYPES.PROXIED" ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-if="app.manifest.addons.localstorage" ng-href="{{ '/filemanager.html?type=app&id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
|
||||
<a class="btn btn-sm btn-default" ng-if="app.manifest.addons.localstorage" ng-href="{{ '/filemanager/#/home/app/' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
|
||||
</div>
|
||||
<div class="dropdown" style="display: inline-block">
|
||||
<button class="btn btn-sm btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'app.docsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom">
|
||||
|
||||
@@ -45,6 +45,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
// If new categories added make sure the translation below exists
|
||||
$scope.categories = [
|
||||
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
|
||||
{ id: 'automation', icon: 'fa fa-robot', label: 'Automation'},
|
||||
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
|
||||
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
|
||||
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<div class="modal-body">{{ 'backups.cleanupBackups.description' | tr }}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="createBackup.startCleanup()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="cleanupBackups.start()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -468,7 +468,7 @@
|
||||
<div class="col-xs-6 text-right no-wrap">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
|
||||
<span ng-show="mountlike(backupConfig.provider)">
|
||||
<i class="fa fa-circle" ng-style="{ color: backupConfig.mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="backupConfig.mountStatus" uib-tooltip="{{ backupConfig.mountStatus.message }}"></i>
|
||||
<i class="fa fa-circle" ng-style="{ color: mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="mountStatus" uib-tooltip="{{ mountStatus.message }}"></i>
|
||||
<span ng-show="backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</span>
|
||||
@@ -507,8 +507,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.schedule.title' | tr }}</h3>
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'backups.schedule.title' | tr }}
|
||||
<!-- <a class="btn btn-sm btn-default pull-right" ng-href="/logs.html?taskId={{cleanupBackups.taskId}}" target="_blank" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}"><i class="fas fa-align-left"></i></a> -->
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="cleanupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in cleanupTasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
@@ -532,13 +547,28 @@
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-default" ng-click="cleanupBackups.ask()" ng-disabled="cleanupBackups.busy" style="margin-right: 5px"><i class="fa fa-circle-notch fa-spin" ng-show="cleanupBackups.busy"></i> {{ 'backups.listing.cleanupBackups' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureScheduleAndRetention.show()">{{ 'backups.schedule.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.listing.title' | tr }}</h3>
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'backups.listing.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="backupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in backupTasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
@@ -594,23 +624,9 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-default" ng-click="createBackup.cleanupBackups()" ng-show="!createBackup.busy" style="margin-right: 5px">{{ 'backups.listing.cleanupBackups' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy">{{ 'backups.listing.backupNow' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopTask()" ng-show="createBackup.busy">{{ 'backups.listing.stopTask' | tr:{ taskType: createBackup.taskType } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.logs.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'backups.logs.description' | tr }}</p>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{createBackup.taskId}}" ng-disabled="!createBackup.taskId" target="_blank">{{ 'backups.logs.showLogs' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+100
-27
@@ -12,11 +12,13 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.memory = null; // { memory, swap }
|
||||
|
||||
$scope.mountStatus = null; // { state, message }
|
||||
$scope.manualBackupApps = [];
|
||||
|
||||
$scope.backupConfig = {};
|
||||
$scope.backups = [];
|
||||
$scope.backupTasks = [];
|
||||
$scope.cleanupTasks = [];
|
||||
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
$scope.wasabiRegions = REGIONS_WASABI;
|
||||
@@ -119,11 +121,9 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
taskType: TASK_TYPES.TASK_BACKUP,
|
||||
|
||||
checkStatus: function () {
|
||||
// TODO support both task types TASK_BACKUP and TASK_CLEAN_BACKUPS
|
||||
Client.getLatestTaskByType($scope.createBackup.taskType, function (error, task) {
|
||||
init: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_BACKUP, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
@@ -143,6 +143,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.createBackup.percent = 100; // indicates that 'result' is valid
|
||||
$scope.createBackup.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
getBackupTasks();
|
||||
|
||||
return fetchBackups();
|
||||
}
|
||||
|
||||
@@ -158,7 +160,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.createBackup.percent = 0;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.errorMessage = '';
|
||||
$scope.createBackup.taskType = TASK_TYPES.TASK_BACKUP;
|
||||
|
||||
Client.startBackup(function (error, taskId) {
|
||||
if (error) {
|
||||
@@ -177,32 +178,14 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.createBackup.taskId = taskId;
|
||||
$scope.createBackup.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
cleanupBackups: function () {
|
||||
$('#cleanupBackupsModal').modal('show');
|
||||
},
|
||||
|
||||
startCleanup: function () {
|
||||
$scope.createBackup.busy = true;
|
||||
$scope.createBackup.percent = 0;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.errorMessage = '';
|
||||
$scope.createBackup.taskType = TASK_TYPES.TASK_CLEAN_BACKUPS;
|
||||
|
||||
$('#cleanupBackupsModal').modal('hide');
|
||||
|
||||
Client.cleanupBackups(function (error, taskId) {
|
||||
if (error) console.error(error);
|
||||
getBackupTasks();
|
||||
|
||||
$scope.createBackup.taskId = taskId;
|
||||
$scope.createBackup.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
stopTask: function () {
|
||||
Client.stopTask($scope.createBackup.taskId, function (error) {
|
||||
if (error) {
|
||||
@@ -214,6 +197,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
|
||||
$scope.createBackup.busy = false;
|
||||
getBackupTasks();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -221,6 +205,62 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cleanupBackups = {
|
||||
busy: false,
|
||||
taskId: 0,
|
||||
|
||||
init: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.cleanupBackups.taskId = task.id;
|
||||
$scope.cleanupBackups.updateStatus();
|
||||
|
||||
getCleanupTasks();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.cleanupBackups.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.cleanupBackups.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.cleanupBackups.busy = false;
|
||||
|
||||
getCleanupTasks();
|
||||
fetchBackups();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.cleanupBackups.busy = true;
|
||||
$scope.cleanupBackups.message = data.message;
|
||||
window.setTimeout($scope.cleanupBackups.updateStatus, 3000);
|
||||
});
|
||||
},
|
||||
|
||||
ask: function () {
|
||||
$('#cleanupBackupsModal').modal('show');
|
||||
},
|
||||
|
||||
start: function () {
|
||||
$scope.cleanupBackups.busy = true;
|
||||
|
||||
$('#cleanupBackupsModal').modal('hide');
|
||||
|
||||
Client.cleanupBackups(function (error, taskId) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.cleanupBackups.taskId = taskId;
|
||||
$scope.cleanupBackups.updateStatus();
|
||||
|
||||
getCleanupTasks();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.listBackups = {
|
||||
};
|
||||
|
||||
@@ -727,6 +767,35 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backupConfig = backupConfig;
|
||||
$scope.mountStatus = null;
|
||||
|
||||
if (!$scope.mountlike($scope.backupConfig.provider)) return;
|
||||
|
||||
Client.getBackupMountStatus(function (error, mountStatus) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mountStatus = mountStatus;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupTasks() {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_BACKUP, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!tasks.length) return;
|
||||
|
||||
$scope.backupTasks = tasks.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
function getCleanupTasks() {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!tasks.length) return;
|
||||
|
||||
$scope.cleanupTasks = tasks.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -742,7 +811,11 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return app.type !== APP_TYPES.LINK && !app.enableBackup; });
|
||||
|
||||
// show backup status
|
||||
$scope.createBackup.checkStatus();
|
||||
$scope.createBackup.init();
|
||||
$scope.cleanupBackups.init();
|
||||
|
||||
getBackupTasks();
|
||||
getCleanupTasks();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -331,9 +331,9 @@
|
||||
{{ prettyProviderName(domain) }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" title="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" title="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" title="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" uib-tooltip="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" uib-tooltip="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" uib-tooltip="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -350,8 +350,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'domains.renewCerts.title' | tr }}</h3>
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.renewCerts.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="renewCerts.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in renewCerts.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -375,14 +389,27 @@
|
||||
<p ng-hide="renewCerts.busy">
|
||||
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
|
||||
</p>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{renewCerts.taskId}}" ng-disabled="!renewCerts.taskId" target="_blank">{{ 'domains.renewCerts.showLogsAction' | tr }}</a>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy" style="margin-right: 10px">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'domains.syncDns.title' | tr }}</h3>
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.syncDns.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="syncDns.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in syncDns.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -406,14 +433,27 @@
|
||||
<p ng-hide="syncDns.busy">
|
||||
<div class="has-error" ng-show="!syncDns.active">{{ syncDns.errorMessage }}</div>
|
||||
</p>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{syncDns.taskId}}" ng-disabled="!syncDns.taskId" target="_blank">{{ 'domains.syncDns.showLogsAction' | tr }}</a>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy" style="margin-right: 10px">{{ 'domains.syncDns.syncAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy">{{ 'domains.syncDns.syncAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'domains.changeDashboardDomain.title' | tr }}</h3>
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'domains.changeDashboardDomain.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="changeDashboard.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in changeDashboard.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -447,7 +487,6 @@
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-click="changeDashboard.change()" ng-hide="changeDashboard.busy" ng-disabled="changeDashboard.selectedDomain.domain === changeDashboard.adminDomain.domain">{{ 'domains.changeDashboardDomain.changeAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">{{ 'domains.changeDashboardDomain.cancelAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{changeDashboard.taskId}}" ng-show="changeDashboard.busy" target="_blank">{{ 'domains.changeDashboardDomain.showLogsAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domains = [];
|
||||
$scope.ready = false;
|
||||
$scope.domainSearchString = '';
|
||||
$scope.pageSize = 10;
|
||||
$scope.pageSize = localStorage.cloudronPageSize || 10;
|
||||
$scope.currentPage = 1;
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
@@ -489,21 +489,20 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
tasks: [],
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, task) {
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.renewCerts.taskId = task.id;
|
||||
$scope.renewCerts.updateStatus();
|
||||
$scope.renewCerts.tasks = tasks.slice(0, 10);
|
||||
if ($scope.renewCerts.tasks.length && $scope.renewCerts.tasks[0].active) $scope.renewCerts.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.renewCerts.taskId, function (error, data) {
|
||||
var taskId = $scope.renewCerts.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.renewCerts.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
@@ -512,6 +511,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.renewCerts.percent = 100; // indicates that 'result' is valid
|
||||
$scope.renewCerts.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.renewCerts.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -529,15 +530,13 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.renewCerts.errorMessage = '';
|
||||
|
||||
// always rebuild the nginx configs when triggered via the UI. we assume user is clicking this because something is wrong
|
||||
Client.renewCerts({ rebuild: true }, function (error, taskId) {
|
||||
Client.renewCerts({ rebuild: true }, function (error /*, taskId */) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.renewCerts.errorMessage = error.message;
|
||||
|
||||
$scope.renewCerts.busy = false;
|
||||
} else {
|
||||
$scope.renewCerts.taskId = taskId;
|
||||
$scope.renewCerts.updateStatus();
|
||||
$scope.renewCerts.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -548,21 +547,19 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
tasks: [],
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, task) {
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.syncDns.taskId = task.id;
|
||||
$scope.syncDns.updateStatus();
|
||||
$scope.syncDns.tasks = tasks.slice(0, 10);
|
||||
if ($scope.syncDns.tasks.length && $scope.syncDns.tasks[0].active) $scope.syncDns.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.syncDns.taskId, function (error, data) {
|
||||
var taskId = $scope.syncDns.tasks[0].id;
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.syncDns.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
@@ -571,6 +568,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.syncDns.percent = 100; // indicates that 'result' is valid
|
||||
$scope.syncDns.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.syncDns.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -587,15 +586,13 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.errorMessage = '';
|
||||
|
||||
Client.setDnsRecords({}, function (error, taskId) {
|
||||
Client.setDnsRecords({}, function (error /*, taskId */) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.syncDns.errorMessage = error.message;
|
||||
|
||||
$scope.syncDns.busy = false;
|
||||
} else {
|
||||
$scope.syncDns.taskId = taskId;
|
||||
$scope.syncDns.updateStatus();
|
||||
$scope.syncDns.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -649,24 +646,21 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
taskId: '',
|
||||
selectedDomain: null,
|
||||
adminDomain: null,
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SETUP_DNS_AND_CERT, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.changeDashboard.tasks = tasks.slice(0, 10);
|
||||
if ($scope.changeDashboard.tasks.length && $scope.changeDashboard.tasks[0].active) $scope.changeDashboard.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
stop: function () {
|
||||
Client.stopTask($scope.changeDashboard.taskId, function (error) {
|
||||
if (error) console.error(error);
|
||||
$scope.changeDashboard.busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
// this function is not called intentionally. currently, we do switching in two steps - prepare and set
|
||||
// if the user refreshed the UI in the middle of prepare, then it would be awkward to resume/call 'set' when the
|
||||
// user visits the UI the next time around.
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType('prepareDashboardDomain', function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
if (!task) return;
|
||||
|
||||
$scope.changeDashboard.taskId = task.id;
|
||||
$scope.changeDashboard.updateStatus();
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -721,7 +715,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.changeDashboard.busy = false;
|
||||
} else {
|
||||
$scope.changeDashboard.taskId = taskId;
|
||||
$scope.changeDashboard.updateStatus();
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -734,7 +728,9 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.ready = true;
|
||||
});
|
||||
|
||||
$scope.renewCerts.checkStatus();
|
||||
$scope.renewCerts.refreshTasks();
|
||||
$scope.syncDns.refreshTasks();
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
});
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
@@ -725,7 +725,7 @@
|
||||
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p ng-show="record.name === 'MX' && domain.provider === 'namecheap'">{{ 'email.dnsStatus.namecheapInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#namecheap-dns" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/troubleshooting/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.hostname' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].name }}</tt></b></p>
|
||||
<p ng-hide="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.domain' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
|
||||
<p>{{ 'email.dnsStatus.type' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
|
||||
|
||||
@@ -64,6 +64,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
function updateMailUsage(mailboxName, quotaLimit) {
|
||||
if (!$scope.mailUsage) $scope.mailUsage = {};
|
||||
if (!$scope.mailUsage[mailboxName]) $scope.mailUsage[mailboxName] = {};
|
||||
$scope.mailUsage[mailboxName].quotaLimit = quotaLimit;
|
||||
}
|
||||
|
||||
function refreshMailUsage() {
|
||||
Client.getMailUsage($scope.domain.domain, function (error, usage) {
|
||||
if (error) console.error(error);
|
||||
@@ -646,7 +652,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
|
||||
function done() {
|
||||
$scope.mailUsage[$scope.mailboxes.edit.name + '@' + $scope.domain.domain].quotaLimit = $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0; // hack to avoid refresh
|
||||
updateMailUsage($scope.mailboxes.edit.name + '@' + $scope.domain.domain, $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0); // hack to avoid refresh
|
||||
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
$scope.mailboxes.edit.error = null;
|
||||
|
||||
@@ -216,8 +216,6 @@
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-eventlog">{{ 'eventlog.title' | tr }}</a>
|
||||
<!-- hidden for now, until we see a purpose -->
|
||||
<!-- <a class="btn btn-sm btn-default" ng-disabled="user.isAtLeastOwner" href="/filemanager.html?id=mail&type=mail" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a> -->
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -271,7 +269,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'emails.mailboxSharing.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -291,7 +289,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'emails.settings.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
|
||||
{ name: 'app.start', value: 'app.start' },
|
||||
{ name: 'app.stop', value: 'app.stop' },
|
||||
{ name: 'app.restart', value: 'app.restart' },
|
||||
{ name: 'Apptask Crash', value: 'app.task.crash' },
|
||||
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
|
||||
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
|
||||
{ name: 'backup.finish', value: 'backup.finish' },
|
||||
@@ -74,7 +73,6 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
|
||||
{ name: 'volume.add', value: 'volume.add' },
|
||||
{ name: 'volume.update', value: 'volume.update' },
|
||||
{ name: 'volume.remove', value: 'volume.update' },
|
||||
{ name: 'System Crash', value: 'system.crash' }
|
||||
];
|
||||
|
||||
$scope.pageItemCount = [
|
||||
|
||||
@@ -71,6 +71,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Trusted IPs -->
|
||||
<div class="modal fade" id="trustedIpsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'network.trustedIps.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="trustedIpsChangeForm" role="form" novalidate ng-submit="trustedIps.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'network.trustedIpRanges' | tr }}</label>
|
||||
<p class="small">{{ 'network.trustedIps.description' | tr }}</p>
|
||||
<div class="has-error" ng-show="trustedIps.error.trustedIps">{{ trustedIps.error.trustedIps }}</div>
|
||||
<textarea ng-model="trustedIps.trustedIps" placeholder="{{ 'network.firewall.configure.blocklistPlaceholder' | tr }}" name="trustedIps" class="form-control" ng-class="{ 'has-error': !trustedIpsChangeForm.trustedIps.$dirty && trustedIps.error.trustedIps }" rows="4"></textarea>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="trustedIps.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="trustedIps.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal IPv6 -->
|
||||
<div class="modal fade" id="ipv6ConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -173,8 +199,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 -->
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'network.ipv6.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ 'network.ipv6.description' | tr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span>{{ prettyIpProviderName(ipv6Configure.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.provider !== 'noop'">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span ng-show="ipv6Configure.ipv6">{{ ipv6Configure.ipv6 }}</span>
|
||||
<span ng-show="!ipv6Configure.ipv6 && ipv6Configure.serverIPv6">{{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})</span>
|
||||
<span ng-show="ipv6Configure.displayError" class="text-danger">{{ ipv6Configure.displayError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.ifname">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ ipv6Configure.ifname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="ipv6Configure.show()">{{ 'network.ip.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Firewall -->
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'network.firewall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -187,60 +264,19 @@
|
||||
<span>{{ 'network.firewall.blocklist' | tr:{ blockCount: blocklist.currentBlocklistLength } }} <a href="" ng-click="blocklist.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 -->
|
||||
<div class="text-left">
|
||||
<h3>{{ 'network.ipv6.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ 'network.ipv6.description' | tr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span>{{ prettyIpProviderName(ipv6Configure.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.provider !== 'noop'">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span ng-show="ipv6Configure.ipv6">{{ ipv6Configure.ipv6 }}</span>
|
||||
<span ng-show="!ipv6Configure.ipv6 && ipv6Configure.serverIPv6">{{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})</span>
|
||||
<span ng-show="ipv6Configure.displayError" class="text-danger">{{ ipv6Configure.displayError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.ifname">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
|
||||
<span class="text-muted">{{ 'network.trustedIpRanges' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ ipv6Configure.ifname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="ipv6Configure.show()">{{ 'network.ip.configure' | tr }}</button>
|
||||
<span>{{ 'network.trustedIps.summary' | tr:{ trustCount: trustedIps.currentTrustedIpsLength } }} <a href="" ng-click="trustedIps.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<!-- Dynamic DNS -->
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'network.dyndns.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -192,6 +192,50 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.trustedIps = {
|
||||
busy: false,
|
||||
error: {},
|
||||
trustedIps: '',
|
||||
currentTrustedIps: '',
|
||||
currentTrustedIpsLength: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getTrustedIps(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.trustedIps.currentTrustedIps = result;
|
||||
$scope.trustedIps.currentTrustedIpsLength = result.split('\n').filter(function (l) { return l.length !== 0 && l[0] !== '#'; }).length;
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.trustedIps.error = {};
|
||||
$scope.trustedIps.trustedIps = $scope.trustedIps.currentTrustedIps;
|
||||
|
||||
$('#trustedIpsModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.trustedIps.error = {};
|
||||
$scope.trustedIps.busy = true;
|
||||
|
||||
Client.setTrustedIps($scope.trustedIps.trustedIps, function (error) {
|
||||
$scope.trustedIps.busy = false;
|
||||
if (error) {
|
||||
$scope.trustedIps.error.trustedIps = error.message;
|
||||
$scope.trustedIps.error.ip = error.message;
|
||||
$scope.trustedIpsChangeForm.$setPristine();
|
||||
$scope.trustedIpsChangeForm.$setUntouched();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.trustedIps.refresh();
|
||||
|
||||
$('#trustedIpsModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sysinfo = {
|
||||
busy: false,
|
||||
error: {},
|
||||
@@ -276,6 +320,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
|
||||
$scope.dyndnsConfigure.refresh();
|
||||
$scope.ipv6Configure.refresh();
|
||||
$scope.trustedIps.refresh();
|
||||
if ($scope.user.isAtLeastOwner) $scope.blocklist.refresh();
|
||||
});
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'settings.timezone.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'settings.language.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -251,8 +251,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'settings.updates.title' | tr }}</h3>
|
||||
<div class="text-left section-header">
|
||||
<h3>
|
||||
{{ 'settings.updates.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="update.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'settings.updates.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in update.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
@@ -286,7 +300,6 @@
|
||||
<div class="row" ng-show="update.busy">
|
||||
<div class="col-md-12">
|
||||
<p >{{ update.message }}</p>
|
||||
<p class="has-error" ng-show="update.errorMessage">{{ update.errorMessage }}. <a ng-class="warning" ng-href="/logs.html?taskId={{update.taskId}}" target="_blank">{{ 'settings.updates.showLogsAction' | tr }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -300,7 +313,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'settings.privateDockerRegistry.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global $:false, TASK_TYPES */
|
||||
|
||||
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$translate', '$rootScope', '$timeout', 'Client', function ($scope, $location, $translate, $rootScope, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -87,8 +87,16 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
percent: 0,
|
||||
message: 'Downloading',
|
||||
errorMessage: '', // this shows inline
|
||||
taskId: '',
|
||||
skipBackup: false,
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_UPDATE, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.update.tasks = tasks.slice(0, 10);
|
||||
if ($scope.update.tasks.length && $scope.update.tasks[0].active) $scope.update.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
checkNow: function () {
|
||||
$scope.update.checking = true;
|
||||
@@ -108,7 +116,9 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
},
|
||||
|
||||
stopUpdate: function () {
|
||||
Client.stopTask($scope.update.taskId, function (error) {
|
||||
var taskId = $scope.update.tasks[0].id;
|
||||
|
||||
Client.stopTask(taskId, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409) {
|
||||
$scope.update.errorMessage = 'No update is currently in progress';
|
||||
@@ -124,16 +134,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
});
|
||||
},
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType('update', function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
if (!task) return;
|
||||
|
||||
$scope.update.taskId = task.id;
|
||||
$scope.update.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
reloadIfNeeded: function () {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return $scope.error(error);
|
||||
@@ -143,7 +143,9 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.update.taskId, function (error, data) {
|
||||
var taskId = $scope.update.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.update.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
@@ -154,6 +156,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
|
||||
if (!data.errorMessage) $scope.update.reloadIfNeeded(); // assume success
|
||||
|
||||
$scope.update.refreshTasks(); // redundant... update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,7 +176,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
$scope.update.message = '';
|
||||
$scope.update.errorMessage = '';
|
||||
|
||||
Client.update({ skipBackup: $scope.update.skipBackup }, function (error, taskId) {
|
||||
Client.update({ skipBackup: $scope.update.skipBackup }, function (error /*, taskId */) {
|
||||
if (error) {
|
||||
$scope.update.error.generic = error.message;
|
||||
$scope.update.busy = false;
|
||||
@@ -181,8 +185,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
|
||||
$('#updateModal').modal('hide');
|
||||
|
||||
$scope.update.taskId = taskId;
|
||||
$scope.update.updateStatus();
|
||||
$scope.update.refreshTasks();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -430,7 +433,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
});
|
||||
});
|
||||
|
||||
$scope.update.checkStatus();
|
||||
$scope.update.refreshTasks();
|
||||
|
||||
if ($scope.user.isAtLeastOwner) getSubscription();
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'support.remoteSupport.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -672,6 +672,7 @@
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showPrevPage()" ng-class="{ 'btn-primary': currentPage > 1 }" ng-disabled="userRefreshBusy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<span style="margin: 0 5px; line-height: 1.5; font-size: 12px;">{{ currentPage }}</span>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showNextPage()" ng-class="{ 'btn-primary': users.length > pageItems }" ng-disabled="userRefreshBusy || users.length < pageItems">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -728,7 +729,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'users.settings.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -762,7 +763,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'users.externalLdap.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -921,7 +922,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'users.exposedLdap.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
@@ -976,7 +977,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'oidc.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
$scope.userSearchString = '';
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageItems = 15;
|
||||
$scope.pageItems = localStorage.cloudronPageSize || 15;
|
||||
$scope.userRefreshBusy = true;
|
||||
|
||||
$scope.userStates = [
|
||||
|
||||
@@ -34,10 +34,14 @@
|
||||
<input type="text" class="form-control" ng-model="volumeAdd.hostPath" name="hostPath" ng-disabled="volumeAdd.busy" placeholder="/mnt/data" ng-required="volumeAdd.mountType === 'mountpoint'" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'ext4' || volumeAdd.mountType === 'xfs'">
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'ext4'">
|
||||
<label class="control-label">{{ 'volumes.addVolumeDialog.diskPath' | tr }}</label>
|
||||
<select class="form-control" ng-model="volumeAdd.diskPath" ng-options="item.path as item.label for item in blockDevices track by item.path"></select>
|
||||
<input type="text" class="form-control" style="margin-top: 5px;" ng-show="volumeAdd.diskPath.path === 'custom'" ng-model="volumeAdd.customDiskPath" ng-disabled="volumeAdd.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="(volumeAdd.mountType === 'ext4' || volumeAdd.mountType === 'xfs') && volumeAdd.diskPath.path === 'custom'">
|
||||
<select class="form-control" ng-model="volumeAdd.ext4Disk" ng-options="item as item.label for item in ext4BlockDevices track by item.path" ng-required="volumeAdd.mountType === 'ext4'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'xfs'">
|
||||
<label class="control-label">{{ 'volumes.addVolumeDialog.diskPath' | tr }}</label>
|
||||
<select class="form-control" ng-model="volumeAdd.xfsDisk" ng-options="item as item.label for item in xfsBlockDevices track by item.path" ng-required="volumeAdd.mountType === 'xfs'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
|
||||
@@ -153,7 +157,7 @@
|
||||
<td class="text-left wrap-table-cell hidden-xs hidden-sm" ng-show="volume.mountType === 'mountpoint' || volume.mountType === 'filesystem'">{{ volume.hostPath }}</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="remount(volume)" ng-show="isMountProvider(volume.mountType)" ng-disabled="volume.remounting" uib-tooltip="{{ 'volumes.remountActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': volume.remounting }"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/filemanager.html?type=volume&id=' + volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/filemanager/#/home/volume/' + volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
<button class="btn btn-xs btn-danger" ng-click="volumeRemove.show(volume)" uib-tooltip="{{ 'volumes.removeVolumeActionTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -88,8 +88,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: {}, // { path, type }
|
||||
customDiskPath: '',
|
||||
ext4Disk: null, // { path, type }
|
||||
xfsDisk: null, // { path, type }
|
||||
user: '',
|
||||
seal: false,
|
||||
port: 22,
|
||||
@@ -105,8 +105,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
$scope.volumeAdd.remoteDir = '';
|
||||
$scope.volumeAdd.username = '';
|
||||
$scope.volumeAdd.password = '';
|
||||
$scope.volumeAdd.diskPath = {};
|
||||
$scope.volumeAdd.customDiskPath = '';
|
||||
$scope.volumeAdd.ext4Disk = null;
|
||||
$scope.volumeAdd.xfsDisk = null;
|
||||
$scope.volumeAdd.user = '';
|
||||
$scope.volumeAdd.seal = false;
|
||||
$scope.volumeAdd.port = 22;
|
||||
@@ -119,7 +119,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
show: function () {
|
||||
$scope.volumeAdd.reset();
|
||||
|
||||
$scope.blockDevices = [];
|
||||
$scope.ext4BlockDevices = [];
|
||||
$scope.xfsBlockDevices = [];
|
||||
|
||||
Client.getBlockDevices(function (error, result) {
|
||||
if (error) console.error('Failed to list blockdevices:', error);
|
||||
@@ -130,11 +131,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
// amend label for UI
|
||||
result.forEach(function (d) { d.label = d.path; });
|
||||
|
||||
// add custom fake option
|
||||
result.push({ path: 'custom', label: 'Custom Path' });
|
||||
|
||||
$scope.blockDevices = result;
|
||||
$scope.volumeAdd.diskPath = $scope.blockDevices[0];
|
||||
$scope.ext4BlockDevices = result.filter(function (d) { return d.type === 'ext4'; });
|
||||
$scope.xfsBlockDevices = result.filter(function (d) { return d.type === 'xfs'; });
|
||||
|
||||
$('#volumeAddModal').modal('show');
|
||||
});
|
||||
@@ -167,9 +165,13 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
user: $scope.volumeAdd.user,
|
||||
privateKey: $scope.volumeAdd.privateKey,
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'ext4' || $scope.volumeAdd.mountType === 'xfs') {
|
||||
} else if ($scope.volumeAdd.mountType === 'ext4') {
|
||||
mountOptions = {
|
||||
diskPath: $scope.volumeAdd.diskPath === 'custom' ? $scope.volumeAdd.customDiskPath : $scope.volumeAdd.diskPath
|
||||
diskPath: $scope.volumeAdd.ext4Disk.path
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'xfs') {
|
||||
mountOptions = {
|
||||
diskPath: $scope.volumeAdd.xfsDisk.path
|
||||
};
|
||||
} else if ($scope.volumeAdd.mountType === 'mountpoint' || $scope.volumeAdd.mountType === 'filesystem') {
|
||||
mountOptions = {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# Vue 3 + Vite
|
||||
# Dashboard Filemanager
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
Local development via:
|
||||
|
||||
## Recommended IDE Setup
|
||||
```
|
||||
VITE_API_ORIGIN=my.nebulon.space npm run dev
|
||||
```
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
It requires an access token in `localStorage.token`.
|
||||
|
||||
The default local url looks like `http://localhost:5173/#/home/app/<appId>`
|
||||
|
||||
Generated
+192
-109
@@ -8,24 +8,26 @@
|
||||
"name": "my-vue-app",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans": "^5.0.3",
|
||||
"combokeys": "^3.0.1",
|
||||
"filesize": "^10.0.7",
|
||||
"pankow": "^0.1.2",
|
||||
"pankow": "^0.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
"primevue": "^3.27.0",
|
||||
"primevue": "^3.29.2",
|
||||
"superagent": "^8.0.9",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.1",
|
||||
"vite": "^4.3.3"
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.20.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz",
|
||||
"integrity": "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==",
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
|
||||
"integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -385,6 +387,73 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/noto-sans": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/noto-sans/-/noto-sans-5.0.3.tgz",
|
||||
"integrity": "sha512-x6M139l0kSik4GcIquZk30yj6fjwBRzqdjcnqSAwCJ0AGk32TqZd1OysHrew31IzHUxUmPoq3YByO+4pDPRBxg=="
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
|
||||
"integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
|
||||
"dependencies": {
|
||||
"@intlify/devtools-if": "9.2.2",
|
||||
"@intlify/message-compiler": "9.2.2",
|
||||
"@intlify/shared": "9.2.2",
|
||||
"@intlify/vue-devtools": "9.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-if": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
|
||||
"integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
|
||||
"integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.2.2",
|
||||
"source-map": "0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
|
||||
"integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/vue-devtools": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
|
||||
"integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.2.2",
|
||||
"@intlify/shared": "9.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
|
||||
@@ -394,9 +463,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.1.tgz",
|
||||
"integrity": "sha512-ZTZjzo7bmxTRTkb8GSTwkPOYDIP7pwuyV+RV53c9PYUouwcbkIZIvWvNWlX2b1dYZqtOv7D6iUAnJLVNGcLrSw==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
|
||||
"integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -407,49 +476,49 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz",
|
||||
"integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
|
||||
"integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/shared": "3.2.47",
|
||||
"@babel/parser": "^7.21.3",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map": "^0.6.1"
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz",
|
||||
"integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
|
||||
"integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.2.47",
|
||||
"@vue/shared": "3.2.47"
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz",
|
||||
"integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
|
||||
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.47",
|
||||
"@vue/compiler-dom": "3.2.47",
|
||||
"@vue/compiler-ssr": "3.2.47",
|
||||
"@vue/reactivity-transform": "3.2.47",
|
||||
"@vue/shared": "3.2.47",
|
||||
"@babel/parser": "^7.20.15",
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/compiler-ssr": "3.3.4",
|
||||
"@vue/reactivity-transform": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7",
|
||||
"magic-string": "^0.30.0",
|
||||
"postcss": "^8.1.10",
|
||||
"source-map": "^0.6.1"
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz",
|
||||
"integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
|
||||
"integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.2.47",
|
||||
"@vue/shared": "3.2.47"
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -458,60 +527,60 @@
|
||||
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz",
|
||||
"integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
|
||||
"integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.2.47"
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz",
|
||||
"integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
|
||||
"integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.47",
|
||||
"@vue/shared": "3.2.47",
|
||||
"@babel/parser": "^7.20.15",
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7"
|
||||
"magic-string": "^0.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz",
|
||||
"integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
|
||||
"integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.2.47",
|
||||
"@vue/shared": "3.2.47"
|
||||
"@vue/reactivity": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz",
|
||||
"integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
|
||||
"integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
|
||||
"dependencies": {
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"@vue/shared": "3.2.47",
|
||||
"csstype": "^2.6.8"
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"csstype": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz",
|
||||
"integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
|
||||
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.2.47",
|
||||
"@vue/shared": "3.2.47"
|
||||
"@vue/compiler-ssr": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.2.47"
|
||||
"vue": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz",
|
||||
"integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ=="
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
|
||||
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
@@ -562,9 +631,9 @@
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "2.6.21",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
|
||||
"integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
@@ -755,11 +824,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
|
||||
"version": "0.30.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
|
||||
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
|
||||
"dependencies": {
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
@@ -801,9 +873,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.37.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.37.1.tgz",
|
||||
"integrity": "sha512-jLXEEYSbqMkT/FuJLBZAVWGuhIb4JNwHE9kPTorAVmsdZ4UzHAfgWxLsVtD7pLRFaOwYPhNG9nUCpmFL1t/dIg=="
|
||||
"version": "0.39.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.39.0.tgz",
|
||||
"integrity": "sha512-zhbZ2Nx93tLR8aJmL2zI1mhJpsl87HMebNBM6R8z4pLfs8pj604pIVIVwyF1TivcfNtIPpMXL+nb3DsBmE/x6Q=="
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
@@ -844,13 +916,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pankow": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pankow/-/pankow-0.1.2.tgz",
|
||||
"integrity": "sha512-JrVaqnIKzH762AAjxAyRMW4T/Fm0DhN90aT57Geukb2g8WE7qhBlSOgcFCFu+4U9SGUSy3mIRJaq1K1jdjFXiA==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pankow/-/pankow-0.3.1.tgz",
|
||||
"integrity": "sha512-/h5TuUI4M8XiCPXLqJjF95ZGIvq8KVt9N2ExHqzMo01YMkYVFFU4hyS7pudCtlS6u0+syDWUWL/qml8mIhJOrw==",
|
||||
"dependencies": {
|
||||
"filesize": "^10.0.7",
|
||||
"monaco-editor": "^0.37.1",
|
||||
"primevue": "^3.27.0",
|
||||
"monaco-editor": "^0.39.0",
|
||||
"primevue": "^3.29.2",
|
||||
"superagent": "^8.0.9"
|
||||
}
|
||||
},
|
||||
@@ -892,9 +964,9 @@
|
||||
"integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA=="
|
||||
},
|
||||
"node_modules/primevue": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-3.27.0.tgz",
|
||||
"integrity": "sha512-oVJl8vLGNb6t5nXN41mnjR5V9Cc/eHVvmtRWiNgIC1db6OW3Qo7y2LaDEmXps/wdxX/FuJ7nuPHAZI4y8tvGyQ==",
|
||||
"version": "3.29.2",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-3.29.2.tgz",
|
||||
"integrity": "sha512-zMk1w5AySLR6ipWH2fONKtDabHrvRZhZFI1OdoiBDdMhZpARmWuCXwE/ZFusUh5Te0RBEd3iVCiizjUkSRraRQ==",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
@@ -972,12 +1044,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sourcemap-codec": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
|
||||
"deprecated": "Please use @jridgewell/sourcemap-codec instead"
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz",
|
||||
@@ -999,9 +1065,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.3.tgz",
|
||||
"integrity": "sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==",
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
|
||||
"integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.17.5",
|
||||
@@ -1047,23 +1113,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.2.47",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz",
|
||||
"integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
|
||||
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.2.47",
|
||||
"@vue/compiler-sfc": "3.2.47",
|
||||
"@vue/runtime-dom": "3.2.47",
|
||||
"@vue/server-renderer": "3.2.47",
|
||||
"@vue/shared": "3.2.47"
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"@vue/runtime-dom": "3.3.4",
|
||||
"@vue/server-renderer": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
|
||||
"integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.2.2",
|
||||
"@intlify/shared": "9.2.2",
|
||||
"@intlify/vue-devtools": "9.2.2",
|
||||
"@vue/devtools-api": "^6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
|
||||
"integrity": "sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==",
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz",
|
||||
"integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.4.5"
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
|
||||
@@ -9,17 +9,19 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans": "^5.0.3",
|
||||
"combokeys": "^3.0.1",
|
||||
"filesize": "^10.0.7",
|
||||
"pankow": "^0.1.2",
|
||||
"pankow": "^0.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
"primevue": "^3.27.0",
|
||||
"primevue": "^3.29.2",
|
||||
"superagent": "^8.0.9",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.1",
|
||||
"vite": "^4.3.3"
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="preview-panel">
|
||||
<img :src="item.previewUrl || item.icon" :alt="item.name" :class="{'shadow': item.previewUrl }"/>
|
||||
<img :src="item.previewUrl || item.icon" :alt="item.name" :class="{'shadow': item.previewUrl }" @error="iconError($event)"/>
|
||||
<p>{{ item.name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,7 +10,13 @@
|
||||
export default {
|
||||
name: 'PreviewPanel',
|
||||
props: {
|
||||
item: Object
|
||||
item: Object,
|
||||
fallbackIcon: String
|
||||
},
|
||||
methods: {
|
||||
iconError(event) {
|
||||
event.target.src = this.fallbackIcon;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
// keep in sync with box/src/apps.js
|
||||
const ISTATES = {
|
||||
PENDING_INSTALL: 'pending_install',
|
||||
PENDING_CLONE: 'pending_clone',
|
||||
PENDING_CONFIGURE: 'pending_configure',
|
||||
PENDING_UNINSTALL: 'pending_uninstall',
|
||||
PENDING_RESTORE: 'pending_restore',
|
||||
PENDING_IMPORT: 'pending_import',
|
||||
PENDING_UPDATE: 'pending_update',
|
||||
PENDING_BACKUP: 'pending_backup',
|
||||
PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change
|
||||
PENDING_LOCATION_CHANGE: 'pending_location_change',
|
||||
PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration',
|
||||
PENDING_RESIZE: 'pending_resize',
|
||||
PENDING_DEBUG: 'pending_debug',
|
||||
PENDING_START: 'pending_start',
|
||||
PENDING_STOP: 'pending_stop',
|
||||
PENDING_RESTART: 'pending_restart',
|
||||
ERROR: 'error',
|
||||
INSTALLED: 'installed'
|
||||
};
|
||||
|
||||
const HSTATES = {
|
||||
HEALTHY: 'healthy',
|
||||
UNHEALTHY: 'unhealthy',
|
||||
ERROR: 'error',
|
||||
DEAD: 'dead'
|
||||
};
|
||||
|
||||
const RSTATES ={
|
||||
RUNNING: 'running',
|
||||
STOPPED: 'stopped'
|
||||
};
|
||||
|
||||
const ERROR = {
|
||||
ACCESS_DENIED: 'Access Denied',
|
||||
ALREADY_EXISTS: 'Already Exists',
|
||||
BAD_FIELD: 'Bad Field',
|
||||
COLLECTD_ERROR: 'Collectd Error',
|
||||
CONFLICT: 'Conflict',
|
||||
DATABASE_ERROR: 'Database Error',
|
||||
DNS_ERROR: 'DNS Error',
|
||||
DOCKER_ERROR: 'Docker Error',
|
||||
EXTERNAL_ERROR: 'External Error',
|
||||
FS_ERROR: 'FileSystem Error',
|
||||
INTERNAL_ERROR: 'Internal Error',
|
||||
LOGROTATE_ERROR: 'Logrotate Error',
|
||||
NETWORK_ERROR: 'Network Error',
|
||||
NOT_FOUND: 'Not found',
|
||||
REVERSEPROXY_ERROR: 'ReverseProxy Error',
|
||||
TASK_ERROR: 'Task Error',
|
||||
UNKNOWN_ERROR: 'Unknown Error' // only used for portin,
|
||||
};
|
||||
|
||||
const ROLES = {
|
||||
OWNER: 'owner',
|
||||
ADMIN: 'admin',
|
||||
MAIL_MANAGER: 'mailmanager',
|
||||
USER_MANAGER: 'usermanager',
|
||||
USER: 'user'
|
||||
};
|
||||
|
||||
// sync up with tasks.js
|
||||
const TASK_TYPES = {
|
||||
TASK_APP: 'app',
|
||||
TASK_BACKUP: 'backup',
|
||||
TASK_UPDATE: 'update',
|
||||
TASK_CHECK_CERTS: 'checkCerts',
|
||||
TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert',
|
||||
TASK_CLEAN_BACKUPS: 'cleanBackups',
|
||||
TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap',
|
||||
TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation',
|
||||
TASK_SYNC_DNS_RECORDS: 'syncDnsRecords',
|
||||
TASK_UPDATE_DISK_USAGE: 'updateDiskUsage',
|
||||
};
|
||||
|
||||
const APP_TYPES = {
|
||||
APP: 'app', //default
|
||||
LINK: 'link',
|
||||
PROXIED: 'proxied'
|
||||
};
|
||||
|
||||
// named exports
|
||||
export {
|
||||
APP_TYPES,
|
||||
ERROR,
|
||||
HSTATES,
|
||||
ISTATES,
|
||||
RSTATES,
|
||||
ROLES,
|
||||
TASK_TYPES
|
||||
};
|
||||
|
||||
// default export
|
||||
export default {
|
||||
APP_TYPES,
|
||||
ERROR,
|
||||
HSTATES,
|
||||
ISTATES,
|
||||
RSTATES,
|
||||
ROLES,
|
||||
TASK_TYPES
|
||||
};
|
||||
@@ -1,12 +1,17 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
import './style.css';
|
||||
|
||||
import "@fontsource/noto-sans";
|
||||
|
||||
import 'primevue/resources/themes/saga-blue/theme.css';
|
||||
import 'primevue/resources/primevue.min.css';
|
||||
import 'primeicons/primeicons.css';
|
||||
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import superagent from 'superagent';
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
@@ -26,8 +31,45 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
const translations = {};
|
||||
const i18n = createI18n({
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
messages: translations
|
||||
});
|
||||
|
||||
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html
|
||||
(async function loadLanguages() {
|
||||
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
||||
|
||||
async function loadLanguage(lang) {
|
||||
try {
|
||||
const result = await superagent.get(`${API_ORIGIN}/translation/${lang}.json`);
|
||||
|
||||
// we do not deliver as application/json :/
|
||||
translations[lang] = JSON.parse(result.text);
|
||||
} catch (e) {
|
||||
console.error(`Failed to load language file for ${lang}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
await loadLanguage('en');
|
||||
|
||||
const locale = window.localStorage.NG_TRANSLATE_LANG_KEY;
|
||||
if (locale && locale !== 'en') {
|
||||
await loadLanguage(locale);
|
||||
|
||||
if (i18n.mode === 'legacy') {
|
||||
i18n.global.locale = locale;
|
||||
} else {
|
||||
i18n.global.locale.value = locale;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(i18n);
|
||||
app.use(router);
|
||||
app.use(PrimeVue, { ripple: true });
|
||||
app.use(ConfirmationService);
|
||||
|
||||
@@ -84,6 +84,14 @@ export function createDirectoryModel(origin, accessToken, api) {
|
||||
.send({ action: 'chown', uid: uid, recursive: true })
|
||||
.query({ access_token: accessToken });
|
||||
},
|
||||
async extract(path) {
|
||||
await superagent.put(`${origin}/api/v1/${api}/files/${path}`)
|
||||
.send({ action: 'extract' })
|
||||
.query({ access_token: accessToken });
|
||||
},
|
||||
async download(path) {
|
||||
window.open(`${origin}/api/v1/${api}/files/${path}?download=true&access_token=${accessToken}`);
|
||||
},
|
||||
async save(filePath, content) {
|
||||
const file = new File([content], 'file');
|
||||
await superagent.post(`${origin}/api/v1/${api}/files/${filePath}`)
|
||||
|
||||
@@ -40,3 +40,7 @@ a:hover, a:focus {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.p-button {
|
||||
font-family: Noto Sans !important;
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
|
||||
import { filesize } from 'filesize';
|
||||
|
||||
function prettyDate(value) {
|
||||
var date = new Date(value),
|
||||
diff = (((new Date()).getTime() - date.getTime()) / 1000),
|
||||
day_diff = Math.floor(diff / 86400);
|
||||
|
||||
if (isNaN(day_diff) || day_diff < 0)
|
||||
return;
|
||||
|
||||
return day_diff === 0 && (
|
||||
diff < 60 && 'just now' ||
|
||||
diff < 120 && '1 min ago' ||
|
||||
diff < 3600 && Math.floor( diff / 60 ) + ' min ago' ||
|
||||
diff < 7200 && '1 hour ago' ||
|
||||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
|
||||
day_diff === 1 && 'Yesterday' ||
|
||||
day_diff < 7 && day_diff + ' days ago' ||
|
||||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
|
||||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
|
||||
Math.round( day_diff / 365 ) + ' years ago';
|
||||
}
|
||||
|
||||
function prettyLongDate(value) {
|
||||
if (!value) return 'unkown';
|
||||
|
||||
var date = new Date(value);
|
||||
return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
|
||||
}
|
||||
|
||||
function prettyFileSize(value) {
|
||||
if (typeof value !== 'number') return 'unkown';
|
||||
|
||||
return filesize(value);
|
||||
}
|
||||
|
||||
function sanitize(path) {
|
||||
path = '/' + path;
|
||||
return path.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
function encode(path) {
|
||||
return path.split('/').map(encodeURIComponent).join('/');
|
||||
}
|
||||
|
||||
function decode(path) {
|
||||
return path.split('/').map(decodeURIComponent).join('/');
|
||||
}
|
||||
|
||||
// TODO create share links instead of using access token
|
||||
function getDirectLink(entry) {
|
||||
if (entry.share) {
|
||||
let link = window.location.origin + '/api/v1/shares/' + entry.share.id + '?type=raw&path=' + encodeURIComponent(entry.filePath);
|
||||
return link;
|
||||
} else {
|
||||
return window.location.origin + '/api/v1/files?type=raw&path=' + encodeURIComponent(entry.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO the url might actually return a 412 in which case we have to keep reloading
|
||||
function getPreviewUrl(entry) {
|
||||
if (!entry.previewUrl) return '';
|
||||
return entry.previewUrl;
|
||||
}
|
||||
|
||||
function getShareLink(shareId) {
|
||||
return window.location.origin + '/api/v1/shares/' + shareId + '?type=raw';
|
||||
}
|
||||
|
||||
function download(entries, name) {
|
||||
if (!entries.length) return;
|
||||
|
||||
if (entries.length === 1) {
|
||||
if (entries[0].share) window.location.href = '/api/v1/shares/' + entries[0].share.id + '?type=download&path=' + encodeURIComponent(entries[0].filePath);
|
||||
else window.location.href = '/api/v1/files?type=download&path=' + encodeURIComponent(entries[0].filePath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// be a bit smart about the archive name and folder tree
|
||||
const folderPath = entries[0].filePath.slice(0, -entries[0].fileName.length);
|
||||
const archiveName = name || folderPath.slice(folderPath.slice(0, -1).lastIndexOf('/')+1).slice(0, -1);
|
||||
params.append('name', archiveName);
|
||||
params.append('skipPath', folderPath);
|
||||
|
||||
params.append('entries', JSON.stringify(entries.map(function (entry) {
|
||||
return {
|
||||
filePath: entry.filePath,
|
||||
shareId: entry.share ? entry.share.id : undefined
|
||||
};
|
||||
})));
|
||||
|
||||
window.location.href = '/api/v1/download?' + params.toString();
|
||||
}
|
||||
|
||||
function getFileTypeGroup(entry) {
|
||||
return entry.mimeType.split('/')[0];
|
||||
}
|
||||
|
||||
// simple extension detection, does not work with double extension like .tar.gz
|
||||
function getExtension(entry) {
|
||||
if (entry.isFile) return entry.fileName.slice(entry.fileName.lastIndexOf('.') + 1);
|
||||
return '';
|
||||
}
|
||||
|
||||
function copyToClipboard(value) {
|
||||
var elem = document.createElement('input');
|
||||
elem.value = value;
|
||||
document.body.append(elem);
|
||||
elem.select();
|
||||
document.execCommand('copy');
|
||||
elem.remove();
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
if(document.selection && document.selection.empty) {
|
||||
document.selection.empty();
|
||||
} else if(window.getSelection) {
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
function urlSearchQuery() {
|
||||
return decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
}
|
||||
|
||||
// those paths contain the internal type and path reference eg. shares/:shareId/folder/filename or files/folder/filename
|
||||
function parseResourcePath(resourcePath) {
|
||||
var result = {
|
||||
type: '',
|
||||
path: '',
|
||||
shareId: '',
|
||||
apiPath: '',
|
||||
resourcePath: ''
|
||||
};
|
||||
|
||||
if (resourcePath.indexOf('files/') === 0) {
|
||||
result.type = 'files';
|
||||
result.path = resourcePath.slice('files'.length) || '/';
|
||||
result.apiPath = '/api/v1/files';
|
||||
result.resourcePath = result.type + result.path;
|
||||
} else if (resourcePath.indexOf('shares/') === 0) {
|
||||
result.type = 'shares';
|
||||
result.shareId = resourcePath.split('/')[1];
|
||||
result.path = resourcePath.slice((result.type + '/' + result.shareId).length) || '/';
|
||||
result.apiPath = '/api/v1/shares/' + result.shareId;
|
||||
// without shareId we show the root (share listing)
|
||||
result.resourcePath = result.type + '/' + (result.shareId ? (result.shareId + result.path) : '');
|
||||
} else {
|
||||
console.error('Unknown resource path', resourcePath);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getEntryIdentifier(entry) {
|
||||
return (entry.share ? (entry.share.id + '/') : '') + entry.filePath;
|
||||
}
|
||||
|
||||
function entryListSort(list, prop, desc) {
|
||||
var tmp = list.sort(function (a, b) {
|
||||
var av = a[prop];
|
||||
var bv = b[prop];
|
||||
|
||||
if (typeof av === 'string') return (av.toUpperCase() < bv.toUpperCase()) ? -1 : 1;
|
||||
else return (av < bv) ? -1 : 1;
|
||||
});
|
||||
|
||||
if (desc) return tmp;
|
||||
return tmp.reverse();
|
||||
}
|
||||
|
||||
export {
|
||||
getDirectLink,
|
||||
getPreviewUrl,
|
||||
getShareLink,
|
||||
getFileTypeGroup,
|
||||
prettyDate,
|
||||
prettyLongDate,
|
||||
prettyFileSize,
|
||||
sanitize,
|
||||
encode,
|
||||
decode,
|
||||
download,
|
||||
getExtension,
|
||||
copyToClipboard,
|
||||
clearSelection,
|
||||
urlSearchQuery,
|
||||
parseResourcePath,
|
||||
getEntryIdentifier,
|
||||
entryListSort
|
||||
};
|
||||
+154
-96
@@ -1,26 +1,36 @@
|
||||
<template>
|
||||
<MainLayout>
|
||||
<template #dialogs>
|
||||
<Dialog v-model:visible="fatalError" modal header="Error" :closable="false" :closeOnEscape="false">
|
||||
<p>{{ fatalError }}</p>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="extractInProgress" modal :header="$t('filemanager.extractionInProgress')" :closable="false" :closeOnEscape="false">
|
||||
<div style="text-align: center;">
|
||||
<ProgressSpinner style="width: 50px; height: 50px"/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<!-- have to use v-model instead of : bind - https://github.com/primefaces/primevue/issues/815 -->
|
||||
<Dialog v-model:visible="newFileDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFileDialogNameInput')">
|
||||
<template #header>
|
||||
<label class="dialog-header" for="newFileDialogNameInput">New file name</label>
|
||||
<label class="dialog-header" for="newFileDialogNameInput">{{ $t('filemanager.newFileDialog.title') }}</label>
|
||||
</template>
|
||||
<template #default>
|
||||
<form @submit="onNewFileDialogSubmit" @submit.prevent>
|
||||
<InputText class="dialog-single-input" id="newFileDialogNameInput" v-model="newFileDialog.name" :disabled="newFileDialog.busy" required/>
|
||||
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFileDialog.busy" :disabled="newFileDialog.busy || !newFileDialog.name"/>
|
||||
<Button class="dialog-single-input-submit" type="submit" :label="$t('filemanager.newFileDialog.create')" :loading="newFileDialog.busy" :disabled="newFileDialog.busy || !newFileDialog.name"/>
|
||||
</form>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog v-model:visible="newFolderDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFolderDialogNameInput')">
|
||||
<template #header>
|
||||
<label class="dialog-header" for="newFolderDialogNameInput">New folder name</label>
|
||||
<label class="dialog-header" for="newFolderDialogNameInput">{{ $t('filemanager.newDirectoryDialog.title') }}</label>
|
||||
</template>
|
||||
<template #default>
|
||||
<form @submit="onNewFolderDialogSubmit" @submit.prevent>
|
||||
<InputText class="dialog-single-input" id="newFolderDialogNameInput" v-model="newFolderDialog.name" :disabled="newFolderDialog.busy" required/>
|
||||
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFolderDialog.busy" :disabled="newFolderDialog.busy || !newFolderDialog.name"/>
|
||||
<Button class="dialog-single-input-submit" type="submit" :label="$t('filemanager.newFileDialog.create')" :loading="newFolderDialog.busy" :disabled="newFolderDialog.busy || !newFolderDialog.name"/>
|
||||
</form>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -28,15 +38,17 @@
|
||||
<template #header>
|
||||
<TopBar class="navbar">
|
||||
<template #left>
|
||||
<Button icon="pi pi-chevron-left" @click="onGoUp()" text :disabled="cwd === '/'"/>
|
||||
<span style="margin-left: 20px;">{{ cwd }}</span>
|
||||
<Button icon="pi pi-refresh" @click="onRefresh()" text :loading="busyRefresh" style="margin-right: 5px;"/>
|
||||
<PathBreadcrumbs :path="cwd" :activate-handler="onActivateBreadcrumb"/>
|
||||
</template>
|
||||
<template #right>
|
||||
<Button type="button" label="New" icon="pi pi-plus" @click="onCreateMenu" aria-haspopup="true" aria-controls="create_menu" style="margin-right: 10px" />
|
||||
<Button type="button" :label="$t('filemanager.toolbar.new')" icon="pi pi-plus" @click="onCreateMenu" aria-haspopup="true" aria-controls="create_menu" style="margin-right: 5px" />
|
||||
<Menu ref="createMenu" id="create_menu" :model="createMenuModel" :popup="true" />
|
||||
<Button type="button" label="Upload" icon="pi pi-upload" @click="onUploadMenu" aria-haspopup="true" aria-controls="upload_menu" style="margin-right: 10px" />
|
||||
<Button type="button" :label="$t('filemanager.toolbar.upload')" icon="pi pi-upload" @click="onUploadMenu" aria-haspopup="true" aria-controls="upload_menu" style="margin-right: 5px" />
|
||||
<Menu ref="uploadMenu" id="upload_menu" :model="uploadMenuModel" :popup="true" />
|
||||
<Dropdown v-model="activeResource" filter :options="resourcesDropdownModel" optionLabel="label" optionGroupLabel="label" optionGroupChildren="items" dataKey="id" @change="onAppChange" placeholder="Select an App or Volume" style="margin-right: 10px" />
|
||||
<a class="p-button p-button-secondary" style="margin-left: 20px; margin-right: 5px;" :href="'/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'"><span class="p-button-icon p-button-icon-left pi pi-align-left"></span> {{ $t('filemanager.toolbar.openLogs') }}</a>
|
||||
<a class="p-button p-button-secondary" style="margin-right: 5px;" :href="'/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'"><span class="p-button-icon p-button-icon-left pi pi-desktop"></span> {{ $t('filemanager.toolbar.openTerminal') }}</a>
|
||||
<Button type="button" :label="$t('filemanager.toolbar.restartApp')" severity="secondary" icon="pi pi-sync" @click="onRestartApp" :loading="busyRestart" v-show="resourceType === 'app'"/>
|
||||
</template>
|
||||
</TopBar>
|
||||
</template>
|
||||
@@ -55,6 +67,8 @@
|
||||
:copy-handler="copyHandler"
|
||||
:cut-handler="cutHandler"
|
||||
:paste-handler="pasteHandler"
|
||||
:download-handler="downloadHandler"
|
||||
:extract-handler="extractHandler"
|
||||
:new-file-handler="onNewFile"
|
||||
:new-folder-handler="onNewFolder"
|
||||
:upload-file-handler="onUploadFile"
|
||||
@@ -63,10 +77,12 @@
|
||||
:items="items"
|
||||
:clipboard="clipboard"
|
||||
:owners-model="ownersModel"
|
||||
:fallback-icon="fallbackIcon"
|
||||
:tr="$t"
|
||||
/>
|
||||
</div>
|
||||
<div class="main-view-col" style="max-width: 300px;">
|
||||
<PreviewPanel :item="activeItem || activeDirectoryItem"/>
|
||||
<PreviewPanel :item="activeItem || activeDirectoryItem" :fallback-icon="fallbackIcon"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -75,6 +91,7 @@
|
||||
ref="fileUploader"
|
||||
:upload-handler="uploadHandler"
|
||||
@finished="onUploadFinished"
|
||||
:tr="$t"
|
||||
/>
|
||||
<BottomBar />
|
||||
</template>
|
||||
@@ -87,14 +104,16 @@ import superagent from 'superagent';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Menu from 'primevue/menu';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import { DirectoryView, TopBar, BottomBar, MainLayout, FileUploader } from 'pankow';
|
||||
import { sanitize, buildFilePath } from 'pankow/utils';
|
||||
import { DirectoryView, TopBar, PathBreadcrumbs, BottomBar, MainLayout, FileUploader } from 'pankow';
|
||||
import { sanitize, buildFilePath, sleep } from 'pankow/utils';
|
||||
|
||||
import { ISTATES } from '../constants.js';
|
||||
|
||||
import PreviewPanel from '../components/PreviewPanel.vue';
|
||||
import { createDirectoryModel } from '../models/DirectoryModel.js';
|
||||
@@ -109,17 +128,23 @@ export default {
|
||||
Button,
|
||||
Dialog,
|
||||
DirectoryView,
|
||||
Dropdown,
|
||||
FileUploader,
|
||||
InputText,
|
||||
MainLayout,
|
||||
Menu,
|
||||
PathBreadcrumbs,
|
||||
PreviewPanel,
|
||||
ProgressSpinner,
|
||||
TopBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fallbackIcon: '/mime-types/none.svg',
|
||||
cwd: '/',
|
||||
busyRefresh: false,
|
||||
busyRestart: false,
|
||||
fatalError: false,
|
||||
extractInProgress: false,
|
||||
activeItem: null,
|
||||
activeDirectoryItem: {},
|
||||
items: [],
|
||||
@@ -130,12 +155,9 @@ export default {
|
||||
},
|
||||
accessToken: localStorage.token,
|
||||
apiOrigin: API_ORIGIN || '',
|
||||
apps: [],
|
||||
volumes: [],
|
||||
resources: [],
|
||||
resourcesDropdownModel: [],
|
||||
selectedAppId: '',
|
||||
activeResource: null,
|
||||
title: 'Cloudron',
|
||||
resourceType: '',
|
||||
resourceId: '',
|
||||
visible: true,
|
||||
newFileDialog: {
|
||||
visible: false,
|
||||
@@ -162,20 +184,20 @@ export default {
|
||||
}],
|
||||
// contextMenuModel will have activeItem attached if any command() is called
|
||||
createMenuModel: [{
|
||||
label: 'File',
|
||||
label: () => this.$t('filemanager.toolbar.newFile'),
|
||||
icon: 'pi pi-file',
|
||||
command: this.onNewFile
|
||||
}, {
|
||||
label: 'Folder',
|
||||
label: () => this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'pi pi-folder',
|
||||
command: this.onNewFolder
|
||||
}],
|
||||
uploadMenuModel: [{
|
||||
label: 'File',
|
||||
label: () => this.$t('filemanager.toolbar.uploadFile'),
|
||||
icon: 'pi pi-file',
|
||||
command: this.onUploadFile
|
||||
}, {
|
||||
label: 'Folder',
|
||||
label: () => this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'pi pi-folder',
|
||||
command: this.onUploadFolder
|
||||
}]
|
||||
@@ -183,7 +205,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
cwd(newCwd, oldCwd) {
|
||||
if (this.activeResource) this.$router.push(`/home/${this.activeResource.type}/${this.activeResource.id}${this.cwd}`);
|
||||
if (this.resourceType && this.resourceId) this.$router.push(`/home/${this.resourceType}/${this.resourceId}${this.cwd}`);
|
||||
this.loadCwd();
|
||||
}
|
||||
},
|
||||
@@ -238,8 +260,13 @@ export default {
|
||||
this.activeItem = items[0] || null;
|
||||
this.selectedItems = items;
|
||||
},
|
||||
onGoUp() {
|
||||
this.cwd = sanitize(this.cwd.split('/').slice(0, -1).join('/'));
|
||||
onActivateBreadcrumb(path) {
|
||||
this.cwd = sanitize(path);
|
||||
},
|
||||
async onRefresh() {
|
||||
this.busyRefresh = true;
|
||||
await this.loadCwd();
|
||||
setTimeout(() => { this.busyRefresh = false; }, 500);
|
||||
},
|
||||
async onDrop(targetFolder, dataTransfer) {
|
||||
const fullTargetFolder = sanitize(this.cwd + '/' + targetFolder);
|
||||
@@ -276,9 +303,10 @@ export default {
|
||||
},
|
||||
onItemActivated(item) {
|
||||
if (!item) return;
|
||||
if (item.mimeType === 'inode/symlink') return;
|
||||
|
||||
if (item.type === 'directory') this.cwd = sanitize(this.cwd + '/' + item.name);
|
||||
else this.$router.push(`/viewer/${this.activeResource.type}/${this.activeResource.id}${sanitize(this.cwd + '/' + item.name)}`);
|
||||
else this.$router.push(`/viewer/${this.resourceType}/${this.resourceId}${sanitize(this.cwd + '/' + item.name)}`);
|
||||
},
|
||||
async deleteHandler(files) {
|
||||
if (!files) return;
|
||||
@@ -327,6 +355,15 @@ export default {
|
||||
this.clipboard = {};
|
||||
await this.loadCwd();
|
||||
},
|
||||
async downloadHandler(file) {
|
||||
await this.directoryModel.download(buildFilePath(this.cwd, file.name));
|
||||
},
|
||||
async extractHandler(file) {
|
||||
this.extractInProgress = true;
|
||||
await this.directoryModel.extract(buildFilePath(this.cwd, file.name));
|
||||
await this.loadCwd();
|
||||
this.extractInProgress = false;
|
||||
},
|
||||
async uploadHandler(targetDir, file, progressHandler) {
|
||||
await this.directoryModel.upload(targetDir, file, progressHandler);
|
||||
await this.loadCwd();
|
||||
@@ -335,8 +372,8 @@ export default {
|
||||
this.items = await this.directoryModel.listFiles(this.cwd);
|
||||
|
||||
const tmp = this.cwd.split('/').slice(1);
|
||||
let name = this.activeResource.fqdn;
|
||||
if (tmp.length > 1) name = tmp[tmp.length-2];
|
||||
let name = this.title;
|
||||
if (tmp.length >= 1 && tmp[tmp.length-1]) name = tmp[tmp.length-1];
|
||||
|
||||
this.activeDirectoryItem = {
|
||||
id: name,
|
||||
@@ -346,87 +383,101 @@ export default {
|
||||
icon: `${BASE_URL}mime-types/inode-directory.svg`
|
||||
};
|
||||
},
|
||||
async loadResource(resource) {
|
||||
this.activeResource = resource;
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, resource.type === 'volume' ? `volumes/${resource.id}` : `apps/${resource.id}`);
|
||||
this.loadCwd();
|
||||
async onRestartApp() {
|
||||
if (this.resourceType !== 'app') return;
|
||||
|
||||
this.busyRestart = true;
|
||||
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.post(`${this.apiOrigin}/api/v1/apps/${this.resourceId}/restart`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 202) {
|
||||
console.error(`Failed to restart app ${this.resourceId}`, error || result.statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
while(true) {
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/apps/${this.resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (result && result.statusCode === 200 && result.body.installationState === ISTATES.INSTALLED) break;
|
||||
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
this.busyRestart = false;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
useConfirm();
|
||||
|
||||
// load all apps
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/apps`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error('Failed to list apps', error || result.statusCode);
|
||||
this.apps = [];
|
||||
} else {
|
||||
this.apps = result.body ? result.body.apps.filter(a => !!a.manifest.addons.localstorage) : [];
|
||||
}
|
||||
this.apps.forEach(function (a) { a.type = 'app'; a.label = a.fqdn; });
|
||||
|
||||
// load all volumes
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/volumes`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error('Failed to list volumes', error || result.statusCode);
|
||||
this.volumes = [];
|
||||
} else {
|
||||
this.volumes = result.body ? result.body.volumes : [];
|
||||
}
|
||||
this.volumes.forEach(function (a) { a.type = 'volume'; a.label = a.name; });
|
||||
|
||||
this.resources = this.apps.concat(this.volumes);
|
||||
|
||||
this.resourcesDropdownModel = [{
|
||||
label: 'Apps',
|
||||
items: this.apps
|
||||
}, {
|
||||
label: 'Volumes',
|
||||
items: this.volumes
|
||||
}];
|
||||
|
||||
const type = this.$route.params.type || 'app';
|
||||
const resourceId = this.$route.params.resourceId;
|
||||
const cwd = this.$route.params.cwd;
|
||||
|
||||
if (type === 'volume') {
|
||||
this.activeResource = this.volumes.find(a => a.id === resourceId);
|
||||
if (!this.activeResource) this.activeResource = this.volumes[0];
|
||||
if (!this.activeResource) return console.error('Unable to find volumes', resourceId);
|
||||
} else if (type === 'app') {
|
||||
this.activeResource = this.apps.find(a => a.id === resourceId);
|
||||
if (!this.activeResource) this.activeResource = this.apps[0];
|
||||
if (!this.activeResource) return console.error('Unable to find app', resourceId);
|
||||
if (type === 'app') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/apps/${resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
this.fatalError = `Invalid resource ${type} ${resourceId}`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.title = result.body.label || result.body.fqdn;
|
||||
} else if (type === 'volume') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/volumes/${resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
this.fatalError = `Invalid resource ${type} ${resourceId}`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.title = result.body.name;
|
||||
} else {
|
||||
this.activeResource = this.apps[0];
|
||||
}
|
||||
|
||||
if (!this.activeResource) {
|
||||
console.error('Not able to load apps or volumes. Cannot continue');
|
||||
this.fatalError = `Unsupported type ${type}`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.cwd = sanitize('/' + (this.$route.params.cwd ? this.$route.params.cwd.join('/') : '/'));
|
||||
window.document.title = this.title + ' - File Manager';
|
||||
|
||||
this.loadResource(this.activeResource);
|
||||
this.cwd = sanitize('/' + (cwd ? cwd.join('/') : '/'));
|
||||
this.resourceType = type;
|
||||
this.resourceId = resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, type === 'volume' ? `volumes/${resourceId}` : `apps/${resourceId}`);
|
||||
this.loadCwd();
|
||||
|
||||
this.$watch(() => this.$route.params, (toParams, previousParams) => {
|
||||
if (toParams.type === 'volume') {
|
||||
this.activeResource = this.volumes.find(a => a.id === toParams.resourceId);
|
||||
} else if (toParams.type === 'app') {
|
||||
this.activeResource = this.apps.find(a => a.id === toParams.resourceId);
|
||||
} else {
|
||||
console.error(`Unknown type ${toParams.type}`);
|
||||
if (toParams.type !== 'app' && toParams.type !== 'volume') {
|
||||
this.fatalError = `Unknown type ${toParams.type}`;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((toParams.type !== this.resourceType) || (toParams.resourceId !== this.resourceId)) {
|
||||
this.resourceType = toParams.type;
|
||||
this.resourceId = toParams.resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, toParams.type === 'volume' ? `volumes/${toParams.resourceId}` : `apps/${toParams.resourceId}`);
|
||||
}
|
||||
|
||||
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
|
||||
@@ -467,4 +518,11 @@ export default {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
a.p-button:hover {
|
||||
text-decoration: none;
|
||||
background: #0d89ec;
|
||||
color: #ffffff;
|
||||
border-color: #0d89ec;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-show="active === 'textEditor'"
|
||||
:save-handler="saveHandler"
|
||||
@close="onClose"
|
||||
:tr="$t"
|
||||
/>
|
||||
<ImageViewer ref="imageViewer" v-show="active === 'imageViewer'" @close="onClose"/>
|
||||
</div>
|
||||
@@ -67,6 +68,7 @@ export default {
|
||||
} else {
|
||||
console.warn(`no editor or viewer found for ${this.item.mimeType}`, this.item);
|
||||
this.active = '';
|
||||
window.location.replace(this.directoryModel.getFileUrl(this.filePath));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Generated
+341
-385
File diff suppressed because it is too large
Load Diff
+15
-15
@@ -17,9 +17,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^3.0.2",
|
||||
"@google-cloud/storage": "^6.9.4",
|
||||
"@google-cloud/storage": "^6.10.1",
|
||||
"async": "^3.2.4",
|
||||
"aws-sdk": "^2.1343.0",
|
||||
"aws-sdk": "^2.1377.0",
|
||||
"basic-auth": "^2.0.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"cloudron-manifestformat": "^5.20.0",
|
||||
@@ -36,45 +36,45 @@
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jose": "^4.13.1",
|
||||
"jsdom": "^21.1.1",
|
||||
"jose": "^4.14.4",
|
||||
"jsdom": "^22.0.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"ldapjs": "^2.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.42",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"morgan": "^1.10.0",
|
||||
"multiparty": "^4.2.3",
|
||||
"mysql": "^2.18.1",
|
||||
"nodemailer": "^6.9.1",
|
||||
"nodemailer": "^6.9.2",
|
||||
"nsyslog-parser": "^0.10.1",
|
||||
"oidc-provider": "^7.14.3",
|
||||
"qrcode": "^1.5.1",
|
||||
"oidc-provider": "^8.2.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"readdirp": "^3.6.0",
|
||||
"safetydance": "^2.2.0",
|
||||
"semver": "^7.3.8",
|
||||
"semver": "^7.5.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"superagent": "^8.0.9",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tldjs": "^2.3.1",
|
||||
"ua-parser-js": "^1.0.34",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"underscore": "^1.13.6",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.9.0",
|
||||
"ws": "^8.13.0",
|
||||
"xml2js": "^0.4.23"
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"commander": "^10.0.0",
|
||||
"commander": "^10.0.1",
|
||||
"easy-table": "^1.2.0",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint": "^8.40.0",
|
||||
"expect.js": "*",
|
||||
"hock": "^1.4.1",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"mocha": "^10.2.0",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^13.3.0",
|
||||
"ssh2": "^1.11.0",
|
||||
"nock": "^13.3.1",
|
||||
"ssh2": "^1.13.0",
|
||||
"yesno": "^0.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script tails common logs
|
||||
|
||||
tail -f /home/yellowtent/platformdata/logs/box.log
|
||||
@@ -107,11 +107,14 @@ echo -n "Generating Cloudron Support stats..."
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
echo -e $LINE"DASHBOARD DOMAIN"$LINE >> $OUT
|
||||
mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" &>> $OUT 2>/dev/null || true
|
||||
echo -e $LINE"Linux"$LINE >> $OUT
|
||||
uname -nar &>> $OUT
|
||||
|
||||
echo -e $LINE"PROVIDER"$LINE >> $OUT
|
||||
cat /etc/cloudron/PROVIDER &>> $OUT || true
|
||||
echo -e $LINE"Ubuntu"$LINE >> $OUT
|
||||
lsb_release -a &>> $OUT
|
||||
|
||||
echo -e $LINE"Dashboard Domain"$LINE >> $OUT
|
||||
mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" &>> $OUT 2>/dev/null || true
|
||||
|
||||
echo -e $LINE"Docker container"$LINE >> $OUT
|
||||
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
|
||||
@@ -151,7 +154,4 @@ echo -n "Uploading information..."
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent --data-binary "@$OUT" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
echo ""
|
||||
echo "Please email the following link to support@cloudron.io :"
|
||||
echo ""
|
||||
echo "${PASTEBIN}/${paste_key}"
|
||||
echo -e "\nPlease email the following link to support@cloudron.io : ${PASTEBIN}/${paste_key}"
|
||||
|
||||
@@ -36,8 +36,8 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v16.18.1" ]]; then
|
||||
echo "This script requires node 16.18.1"
|
||||
if [[ "$(node --version)" != "v18.16.0" ]]; then
|
||||
echo "This script requires node 18.16.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -62,7 +62,7 @@ mv "${bundle_dir}/filemanager/dist" "${bundle_dir}/dashboard/dist/filemanager"
|
||||
rm -rf "${bundle_dir}/filemanager"
|
||||
|
||||
echo "==> Installing toplevel node modules"
|
||||
(cd "${bundle_dir}" && npm install --omit=dev --no-optional)
|
||||
(cd "${bundle_dir}" && npm install --omit=dev --omit=optional)
|
||||
|
||||
echo "==> Create final tarball"
|
||||
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
|
||||
|
||||
+9
-24
@@ -72,8 +72,8 @@ readonly is_update=$(systemctl is-active -q box && echo "yes" || echo "no")
|
||||
log "Updating from $(cat $box_src_dir/VERSION 2>/dev/null) to $(cat $box_src_tmp_dir/VERSION 2>/dev/null)"
|
||||
|
||||
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
|
||||
readonly docker_version=20.10.21
|
||||
readonly containerd_version=1.6.10-1
|
||||
readonly docker_version="23.0.6"
|
||||
readonly containerd_version="1.6.21-1"
|
||||
if ! which docker 2>/dev/null || [[ $(docker version --format {{.Client.Version}}) != "${docker_version}" ]]; then
|
||||
log "installing/updating docker"
|
||||
|
||||
@@ -83,8 +83,8 @@ if ! which docker 2>/dev/null || [[ $(docker version --format {{.Client.Version}
|
||||
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon (https://download.docker.com/linux/ubuntu/dists/jammy/pool/stable/amd64/)
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_${containerd_version}_amd64.deb" -o /tmp/containerd.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}-1~ubuntu.${ubuntu_version}~${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}-1~ubuntu.${ubuntu_version}~${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
|
||||
log "installing docker"
|
||||
prepare_apt_once
|
||||
@@ -115,7 +115,8 @@ elif [[ "${ubuntu_version}" == "18.04" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
readonly node_version=16.18.1
|
||||
readonly old_node_version=16.18.1
|
||||
readonly node_version=18.16.0
|
||||
if ! which node 2>/dev/null || [[ "$(node --version)" != "v${node_version}" ]]; then
|
||||
log "installing/updating node ${node_version}"
|
||||
mkdir -p /usr/local/node-${node_version}
|
||||
@@ -124,7 +125,7 @@ if ! which node 2>/dev/null || [[ "$(node --version)" != "v${node_version}" ]];
|
||||
rm /tmp/node.tar.gz
|
||||
ln -sf /usr/local/node-${node_version}/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-${node_version}/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-16.14.2
|
||||
rm -rf /usr/local/node-${old_node_version}
|
||||
fi
|
||||
|
||||
# obsolete module
|
||||
@@ -151,33 +152,17 @@ log "downloading new addon images"
|
||||
images=$(node -e "let i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
log "\tPulling docker images: ${images}"
|
||||
if ! curl --fail --connect-timeout 10 --max-time 10 https://ipv4.api.cloudron.io/api/v1/helper/public_ip; then
|
||||
docker_registry=registry.ipv6.docker.com
|
||||
else
|
||||
docker_registry=registry-1.docker.io
|
||||
fi
|
||||
log "\tUsing ${docker_registry} as the docker registry"
|
||||
|
||||
for image in ${images}; do
|
||||
while ! docker pull "${docker_registry}/${image}"; do # this pulls the image using the sha256
|
||||
while ! docker pull "registry.docker.com/${image}"; do # this pulls the image using the sha256
|
||||
log "Could not pull ${image}"
|
||||
sleep 5
|
||||
done
|
||||
while ! docker pull "${docker_registry}/${image%@sha256:*}"; do # this will tag the image for readability
|
||||
while ! docker pull "registry.docker.com/${image%@sha256:*}"; do # this will tag the image for readability
|
||||
log "Could not pull ${image%@sha256:*}"
|
||||
sleep 5
|
||||
done
|
||||
done
|
||||
|
||||
log "creating cloudron-support user"
|
||||
if ! id cloudron-support 2>/dev/null; then
|
||||
useradd --system --comment "Cloudron Support (support@cloudron.io)" --create-home --no-user-group --shell /bin/bash cloudron-support
|
||||
fi
|
||||
|
||||
log "locking the ${user} account"
|
||||
usermod --shell /usr/sbin/nologin "${user}"
|
||||
passwd --lock "${user}"
|
||||
|
||||
if [[ "${is_update}" == "yes" ]]; then
|
||||
log "stop box service for update"
|
||||
${box_src_dir}/setup/stop.sh
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ readonly ubuntu_version=$(lsb_release -rs)
|
||||
|
||||
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
|
||||
cp -f "${script_dir}/../scripts/cloudron-translation-update" /usr/bin/cloudron-translation-update
|
||||
cp -f "${script_dir}/../scripts/cloudron-logs" /usr/bin/cloudron-logs
|
||||
|
||||
# this needs to match the cloudron/base:2.0.0 gid
|
||||
if ! getent group media; then
|
||||
@@ -66,7 +67,6 @@ mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
|
||||
"${PLATFORM_DATA_DIR}/logs/updater" \
|
||||
"${PLATFORM_DATA_DIR}/logs/tasks" \
|
||||
"${PLATFORM_DATA_DIR}/logs/crash" \
|
||||
"${PLATFORM_DATA_DIR}/logs/collectd"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/sftp/ssh" # sftp keys
|
||||
@@ -166,6 +166,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/nginx/applications/dashboard"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/nginx/cert"
|
||||
cp "${script_dir}/start/nginx/nginx.conf" "${PLATFORM_DATA_DIR}/nginx/nginx.conf"
|
||||
cp "${script_dir}/start/nginx/mime.types" "${PLATFORM_DATA_DIR}/nginx/mime.types"
|
||||
touch "${PLATFORM_DATA_DIR}/nginx/trusted.ips"
|
||||
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
|
||||
# default nginx service file does not restart on crash
|
||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# logrotate config for app, crash, addon and task logs
|
||||
# logrotate config for app, addon and task logs
|
||||
|
||||
# man 7 glob
|
||||
/home/yellowtent/platformdata/logs/graphite/*.log
|
||||
@@ -8,7 +8,6 @@
|
||||
/home/yellowtent/platformdata/logs/postgresql/*.log
|
||||
/home/yellowtent/platformdata/logs/sftp/*.log
|
||||
/home/yellowtent/platformdata/logs/redis-*/*.log
|
||||
/home/yellowtent/platformdata/logs/crash/*.log
|
||||
/home/yellowtent/platformdata/logs/collectd/*.log
|
||||
/home/yellowtent/platformdata/logs/turn/*.log
|
||||
/home/yellowtent/platformdata/logs/updater/*.log {
|
||||
|
||||
@@ -38,6 +38,7 @@ http {
|
||||
# zones for rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
|
||||
|
||||
include trusted.ips;
|
||||
include applications/*.conf;
|
||||
include applications/*/*.conf;
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[Unit]
|
||||
Description=Cloudron Admin
|
||||
OnFailure=crashnotifier@%n.service
|
||||
After=mysql.service nginx.service
|
||||
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
|
||||
Wants=cloudron-resize-fs.service
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# http://northernlightlabs.se/systemd.status.mail.on.unit.failure
|
||||
[Unit]
|
||||
Description=Cloudron Crash Notifier for %i
|
||||
# otherwise, systemd will kill this unit immediately as nobody requires it
|
||||
StopWhenUnneeded=false
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
ExecStart="/home/yellowtent/box/crashnotifierservice.js" %I
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=50M
|
||||
+25
-29
@@ -21,8 +21,7 @@ const assert = require('assert'),
|
||||
promiseRetry = require('./promise-retry.js'),
|
||||
superagent = require('superagent'),
|
||||
safe = require('safetydance'),
|
||||
users = require('./users.js'),
|
||||
_ = require('underscore');
|
||||
users = require('./users.js');
|
||||
|
||||
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
|
||||
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
@@ -43,7 +42,7 @@ function Acme2(fqdn, domainObject, email) {
|
||||
const prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
||||
this.caDirectory = prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
|
||||
this.directory = {};
|
||||
this.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
||||
this.forceHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
||||
this.wildcard = !!domainObject.tlsConfig.wildcard;
|
||||
this.domain = domainObject.domain;
|
||||
|
||||
@@ -58,7 +57,7 @@ function Acme2(fqdn, domainObject, email) {
|
||||
|
||||
this.certName = this.cn.replace('*.', '_.');
|
||||
|
||||
debug(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.performHttpAuthorization}`);
|
||||
debug(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.forceHttpAuthorization}`);
|
||||
}
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
@@ -114,7 +113,7 @@ Acme2.prototype.sendSignedRequest = async function (url, payload) {
|
||||
|
||||
debug(`sendSignedRequest: using nonce ${nonce} for url ${url}`);
|
||||
|
||||
const protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
const protected64 = b64(JSON.stringify(Object.assign({}, header, { nonce: nonce })));
|
||||
|
||||
const signer = crypto.createSign('RSA-SHA256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
@@ -370,13 +369,8 @@ Acme2.prototype.downloadCertificate = async function (certUrl) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.prepareHttpChallenge = async function (authorization) {
|
||||
assert.strictEqual(typeof authorization, 'object');
|
||||
|
||||
debug(`prepareHttpChallenge: challenges: ${JSON.stringify(authorization)}`);
|
||||
const httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges');
|
||||
const challenge = httpChallenges[0];
|
||||
Acme2.prototype.prepareHttpChallenge = async function (challenge) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
debug(`prepareHttpChallenge: preparing for challenge ${JSON.stringify(challenge)}`);
|
||||
|
||||
@@ -386,8 +380,6 @@ Acme2.prototype.prepareHttpChallenge = async function (authorization) {
|
||||
debug(`prepareHttpChallenge: writing ${keyAuthorization} to ${challengeFilePath}`);
|
||||
|
||||
if (!safe.fs.writeFileSync(challengeFilePath, keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, `Error writing challenge: ${safe.error.message}`);
|
||||
|
||||
return challenge;
|
||||
};
|
||||
|
||||
Acme2.prototype.cleanupHttpChallenge = async function (challenge) {
|
||||
@@ -416,13 +408,10 @@ function getChallengeSubdomain(cn, domain) {
|
||||
return challengeSubdomain;
|
||||
}
|
||||
|
||||
Acme2.prototype.prepareDnsChallenge = async function (cn, authorization) {
|
||||
assert.strictEqual(typeof authorization, 'object');
|
||||
Acme2.prototype.prepareDnsChallenge = async function (cn, challenge) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
debug(`prepareDnsChallenge: challenges: ${JSON.stringify(authorization)}`);
|
||||
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
|
||||
if (dnsChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no dns challenges');
|
||||
const challenge = dnsChallenges[0];
|
||||
debug(`prepareDnsChallenge: preparing for challenge: ${JSON.stringify(challenge)}`);
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
const shasum = crypto.createHash('sha256');
|
||||
@@ -436,8 +425,6 @@ Acme2.prototype.prepareDnsChallenge = async function (cn, authorization) {
|
||||
await dns.upsertDnsRecords(challengeSubdomain, this.domain, 'TXT', [ `"${txtValue}"` ]);
|
||||
|
||||
await dns.waitForDnsRecord(challengeSubdomain, this.domain, 'TXT', txtValue, { times: 200 });
|
||||
|
||||
return challenge;
|
||||
};
|
||||
|
||||
Acme2.prototype.cleanupDnsChallenge = async function (cn, challenge) {
|
||||
@@ -460,22 +447,31 @@ Acme2.prototype.prepareChallenge = async function (cn, authorization) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
assert.strictEqual(typeof authorization, 'object');
|
||||
|
||||
debug(`prepareChallenge: http: ${this.performHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`);
|
||||
debug(`prepareChallenge: http: ${this.forceHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`);
|
||||
|
||||
if (this.performHttpAuthorization) {
|
||||
return await this.prepareHttpChallenge(authorization);
|
||||
} else {
|
||||
return await this.prepareDnsChallenge(cn, authorization);
|
||||
// validation is cached by LE for 60 days or so. if a user switches from non-wildcard DNS (http challenge) to programmatic DNS (dns challenge), then
|
||||
// LE remembers the challenge type and won't give us a dns challenge for 60 days!
|
||||
// https://letsencrypt.org/docs/faq/#i-successfully-renewed-a-certificate-but-validation-didn-t-happen-this-time-how-is-that-possible
|
||||
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
|
||||
const httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
|
||||
if (this.forceHttpAuthorization || dnsChallenges.length === 0) {
|
||||
if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges');
|
||||
await this.prepareHttpChallenge(httpChallenges[0]);
|
||||
return httpChallenges[0];
|
||||
}
|
||||
|
||||
await this.prepareDnsChallenge(cn, dnsChallenges[0]);
|
||||
return dnsChallenges[0];
|
||||
};
|
||||
|
||||
Acme2.prototype.cleanupChallenge = async function (cn, challenge) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
|
||||
debug(`cleanupChallenge: http: ${this.forceHttpAuthorization}`);
|
||||
|
||||
if (this.performHttpAuthorization) {
|
||||
if (this.forceHttpAuthorization) {
|
||||
await this.cleanupHttpChallenge(challenge);
|
||||
} else {
|
||||
await this.cleanupDnsChallenge(cn, challenge);
|
||||
|
||||
@@ -93,7 +93,7 @@ async function checkAppHealth(app, options) {
|
||||
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
|
||||
.redirects(0)
|
||||
.ok(() => true)
|
||||
.timeout(options.timeout * 1000));
|
||||
.timeout(options.timeout));
|
||||
|
||||
if (healthCheckError) {
|
||||
await apps.appendLogLine(app, `=> Healtheck error: ${healthCheckError}`);
|
||||
|
||||
+3
-3
@@ -112,11 +112,11 @@ async function detectMetaInfo(applink) {
|
||||
debug(`detectMetaInfo: found icon: ${favicon}`);
|
||||
|
||||
const [error, response] = await safe(superagent.get(favicon));
|
||||
if (error) console.error(`Failed to fetch icon ${favicon}: `, error);
|
||||
if (error) debug(`Failed to fetch icon ${favicon}: `, error);
|
||||
else if (response.ok && response.headers['content-type'] === 'image/png') applink.icon = response.body;
|
||||
else console.error(`Failed to fetch icon ${favicon}: statusCode=${response.status}`);
|
||||
else debug(`Failed to fetch icon ${favicon}: statusCode=${response.status}`);
|
||||
} else {
|
||||
console.error(`Unable to find a suitable icon for ${applink.upstreamUri}`);
|
||||
debug(`Unable to find a suitable icon for ${applink.upstreamUri}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -1243,7 +1243,7 @@ async function addTask(appId, installationState, task, auditSource) {
|
||||
|
||||
const taskId = await tasks.add(tasks.TASK_APP, [ appId, args ]);
|
||||
|
||||
const [updateError] = await safe(setTask(appId, _.extend({ installationState, taskId, error: null }, values), { requiredState, requireNullTaskId }));
|
||||
const [updateError] = await safe(setTask(appId, Object.assign({ installationState, taskId, error: null }, values), { requiredState, requireNullTaskId }));
|
||||
if (updateError && updateError.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.BAD_STATE, 'Another task is scheduled for this app'); // could be because app went away OR a taskId exists
|
||||
if (updateError) throw updateError;
|
||||
|
||||
@@ -1378,9 +1378,9 @@ async function install(data, auditSource) {
|
||||
}
|
||||
|
||||
const locations = [{ subdomain, domain, type: exports.LOCATION_TYPE_PRIMARY }]
|
||||
.concat(secondaryDomains.map(ad => _.extend(ad, { type: exports.LOCATION_TYPE_SECONDARY })))
|
||||
.concat(redirectDomains.map(ad => _.extend(ad, { type: exports.LOCATION_TYPE_REDIRECT })))
|
||||
.concat(aliasDomains.map(ad => _.extend(ad, { type: exports.LOCATION_TYPE_ALIAS })));
|
||||
.concat(secondaryDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_SECONDARY })))
|
||||
.concat(redirectDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_REDIRECT })))
|
||||
.concat(aliasDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_ALIAS })));
|
||||
|
||||
error = await validateLocations(locations);
|
||||
if (error) throw error;
|
||||
@@ -1426,7 +1426,7 @@ async function install(data, auditSource) {
|
||||
|
||||
const taskId = await addTask(appId, app.installationState, task, auditSource);
|
||||
|
||||
const newApp = _.extend({}, _.omit(app, 'icon'), { appStoreId, manifest, subdomain, domain, portBindings });
|
||||
const newApp = Object.assign({}, _.omit(app, 'icon'), { appStoreId, manifest, subdomain, domain, portBindings });
|
||||
newApp.fqdn = dns.fqdn(newApp.subdomain, newApp.domain);
|
||||
newApp.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
newApp.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
@@ -1484,7 +1484,7 @@ async function setUpstreamUri(app, upstreamUri, auditSource) {
|
||||
const error = validateUpstreamUri(upstreamUri);
|
||||
if (error) throw error;
|
||||
|
||||
await reverseProxy.writeAppConfigs(_.extend({}, app, { upstreamUri }));
|
||||
await reverseProxy.writeAppConfigs(Object.assign({}, app, { upstreamUri }));
|
||||
|
||||
await update(appId, { upstreamUri });
|
||||
|
||||
@@ -1755,7 +1755,7 @@ async function setReverseProxyConfig(app, reverseProxyConfig, auditSource) {
|
||||
assert.strictEqual(typeof reverseProxyConfig, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
reverseProxyConfig = _.extend({ robotsTxt: null, csp: null, hstsPreload: false }, reverseProxyConfig);
|
||||
reverseProxyConfig = Object.assign({ robotsTxt: null, csp: null, hstsPreload: false }, reverseProxyConfig);
|
||||
|
||||
const appId = app.id;
|
||||
let error = validateCsp(reverseProxyConfig.csp);
|
||||
@@ -1764,7 +1764,7 @@ async function setReverseProxyConfig(app, reverseProxyConfig, auditSource) {
|
||||
error = validateRobotsTxt(reverseProxyConfig.robotsTxt);
|
||||
if (error) throw error;
|
||||
|
||||
await reverseProxy.writeAppConfigs(_.extend({}, app, { reverseProxyConfig }));
|
||||
await reverseProxy.writeAppConfigs(Object.assign({}, app, { reverseProxyConfig }));
|
||||
|
||||
await update(appId, { reverseProxyConfig });
|
||||
|
||||
@@ -1852,9 +1852,9 @@ async function setLocation(app, data, auditSource) {
|
||||
}
|
||||
|
||||
const locations = [{ subdomain: values.subdomain, domain: values.domain, type: exports.LOCATION_TYPE_PRIMARY }]
|
||||
.concat(values.secondaryDomains.map(ad => _.extend(ad, { type: exports.LOCATION_TYPE_SECONDARY })))
|
||||
.concat(values.redirectDomains.map(ad => _.extend(ad, { type: exports.LOCATION_TYPE_REDIRECT })))
|
||||
.concat(values.aliasDomains.map(ad => _.extend(ad, { type: exports.LOCATION_TYPE_ALIAS })));
|
||||
.concat(values.secondaryDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_SECONDARY })))
|
||||
.concat(values.redirectDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_REDIRECT })))
|
||||
.concat(values.aliasDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_ALIAS })));
|
||||
|
||||
error = await validateLocations(locations);
|
||||
if (error) throw error;
|
||||
@@ -1876,7 +1876,7 @@ async function setLocation(app, data, auditSource) {
|
||||
values.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
values.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId }, values));
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, Object.assign({ appId, app, taskId }, values));
|
||||
|
||||
return { taskId };
|
||||
}
|
||||
@@ -2085,7 +2085,7 @@ async function repair(app, data, auditSource) {
|
||||
} else {
|
||||
errorState = exports.ISTATE_PENDING_CONFIGURE;
|
||||
if (data.dockerImage) {
|
||||
let newManifest = _.extend({}, app.manifest, { dockerImage: data.dockerImage });
|
||||
let newManifest = Object.assign({}, app.manifest, { dockerImage: data.dockerImage });
|
||||
task.values.manifest = newManifest;
|
||||
}
|
||||
}
|
||||
@@ -2269,7 +2269,7 @@ async function clone(app, data, user, auditSource) {
|
||||
const secondaryDomains = translateSecondaryDomains(data.secondaryDomains || {});
|
||||
|
||||
const locations = [{ subdomain, domain, type: exports.LOCATION_TYPE_PRIMARY }]
|
||||
.concat(secondaryDomains.map(ad => _.extend(ad, { type: exports.LOCATION_TYPE_SECONDARY })));
|
||||
.concat(secondaryDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_SECONDARY })));
|
||||
|
||||
error = await validateLocations(locations);
|
||||
if (error) throw error;
|
||||
@@ -2327,7 +2327,7 @@ async function clone(app, data, user, auditSource) {
|
||||
};
|
||||
const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, auditSource);
|
||||
|
||||
const newApp = _.extend({}, _.omit(obj, 'icon'), { appStoreId, manifest, subdomain, domain, portBindings });
|
||||
const newApp = Object.assign({}, _.omit(obj, 'icon'), { appStoreId, manifest, subdomain, domain, portBindings });
|
||||
newApp.fqdn = dns.fqdn(newApp.subdomain, newApp.domain);
|
||||
newApp.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
newApp.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
|
||||
+4
-4
@@ -258,7 +258,7 @@ async function moveDataDir(app, targetVolumeId, targetVolumePrefix) {
|
||||
assert(targetVolumePrefix === null || typeof targetVolumePrefix === 'string');
|
||||
|
||||
const resolvedSourceDir = await apps.getStorageDir(app);
|
||||
const resolvedTargetDir = await apps.getStorageDir(_.extend({}, app, { storageVolumeId: targetVolumeId, storageVolumePrefix: targetVolumePrefix }));
|
||||
const resolvedTargetDir = await apps.getStorageDir(Object.assign({}, app, { storageVolumeId: targetVolumeId, storageVolumePrefix: targetVolumePrefix }));
|
||||
|
||||
debug(`moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
|
||||
|
||||
@@ -363,9 +363,9 @@ async function install(app, args, progressCallback) {
|
||||
await progressCallback({ percent: 65, message: 'Downloading backup and restoring addons' });
|
||||
await services.setupAddons(app, app.manifest.addons);
|
||||
await services.clearAddons(app, app.manifest.addons);
|
||||
const backupConfig = restoreConfig.backupConfig; // can be null
|
||||
const backupConfig = restoreConfig.backupConfig;
|
||||
let mountObject = null;
|
||||
if (backupConfig && mounts.isManagedProvider(backupConfig.provider)) {
|
||||
if (mounts.isManagedProvider(backupConfig.provider)) {
|
||||
await progressCallback({ percent: 70, message: 'Setting up mount for importing' });
|
||||
mountObject = { // keep this in sync with importApp in apps.js
|
||||
name: `appimport-${app.id}`,
|
||||
@@ -525,7 +525,7 @@ async function migrateDataDir(app, args, progressCallback) {
|
||||
|
||||
// re-setup addons since this creates the localStorage destination
|
||||
await progressCallback({ percent: 50, message: 'Setting up addons' });
|
||||
await services.setupAddons(_.extend({}, app, { storageVolumeId: newStorageVolumeId, storageVolumePrefix: newStorageVolumePrefix }), app.manifest.addons);
|
||||
await services.setupAddons(Object.assign({}, app, { storageVolumeId: newStorageVolumeId, storageVolumePrefix: newStorageVolumePrefix }), app.manifest.addons);
|
||||
|
||||
await progressCallback({ percent: 60, message: 'Moving data dir' });
|
||||
await moveDataDir(app, newStorageVolumeId, newStorageVolumePrefix);
|
||||
|
||||
@@ -27,6 +27,7 @@ exports = module.exports = {
|
||||
testProviderConfig,
|
||||
|
||||
remount,
|
||||
getMountStatus,
|
||||
|
||||
BACKUP_IDENTIFIER_BOX: 'box',
|
||||
BACKUP_IDENTIFIER_MAIL: 'mail',
|
||||
@@ -50,6 +51,7 @@ const assert = require('assert'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
hat = require('./hat.js'),
|
||||
locker = require('./locker.js'),
|
||||
mounts = require('./mounts.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -361,3 +363,20 @@ async function remount(auditSource) {
|
||||
|
||||
await storage.api(backupConfig.provider).remount(backupConfig);
|
||||
}
|
||||
|
||||
async function getMountStatus() {
|
||||
const backupConfig = await settings.getBackupConfig();
|
||||
|
||||
let hostPath;
|
||||
if (mounts.isManagedProvider(backupConfig.provider)) {
|
||||
hostPath = paths.MANAGED_BACKUP_MOUNT_DIR;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
hostPath = backupConfig.mountPoint;
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
hostPath = backupConfig.backupFolder;
|
||||
} else {
|
||||
throw new BoxError(BoxError.BAD_STATE, 'Backup location is not a mount');
|
||||
}
|
||||
|
||||
return await mounts.getStatus(backupConfig.provider, hostPath); // { state, message }
|
||||
}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ async function checkPreconditions(backupConfig, dataLayout) {
|
||||
let used = 0;
|
||||
for (const localPath of dataLayout.localPaths()) {
|
||||
debug(`checkPreconditions: getting disk usage of ${localPath}`);
|
||||
const result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' });
|
||||
const result = safe.child_process.execSync(`du -Dsb "${localPath}"`, { encoding: 'utf8' });
|
||||
if (!result) throw new BoxError(BoxError.FS_ERROR, `du error: ${safe.error.message}`);
|
||||
used += parseInt(result, 10);
|
||||
}
|
||||
|
||||
+3
-4
@@ -4,8 +4,7 @@
|
||||
|
||||
const assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = BoxError;
|
||||
|
||||
@@ -28,7 +27,7 @@ function BoxError(reason, errorOrMessage, override) {
|
||||
} else { // error object
|
||||
this.message = errorOrMessage.message;
|
||||
this.nestedError = errorOrMessage;
|
||||
_.extend(this, override); // copy enumerable properies
|
||||
Object.assign(this, override); // copy enumerable properies
|
||||
}
|
||||
}
|
||||
util.inherits(BoxError, Error);
|
||||
@@ -70,7 +69,7 @@ BoxError.TIMEOUT = 'Timeout';
|
||||
BoxError.TRY_AGAIN = 'Try Again';
|
||||
|
||||
BoxError.prototype.toPlainObject = function () {
|
||||
return _.extend({}, { message: this.message, reason: this.reason }, this.details);
|
||||
return Object.assign({}, { message: this.message, reason: this.reason }, this.details);
|
||||
};
|
||||
|
||||
// this is a class method for now in case error is not a BoxError
|
||||
|
||||
+4
-14
@@ -35,11 +35,9 @@ const apps = require('./apps.js'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
delay = require('./delay.js'),
|
||||
dns = require('./dns.js'),
|
||||
dockerProxy = require('./dockerproxy.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
logs = require('./logs.js'),
|
||||
mail = require('./mail.js'),
|
||||
@@ -55,6 +53,7 @@ const apps = require('./apps.js'),
|
||||
shell = require('./shell.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
timers = require('timers/promises'),
|
||||
users = require('./users.js');
|
||||
|
||||
const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
|
||||
@@ -86,7 +85,7 @@ async function onActivated(options) {
|
||||
|
||||
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
|
||||
// the UI some time to query the dashboard domain in the restore code path
|
||||
await delay(30000);
|
||||
await timers.setTimeout(30000);
|
||||
await reverseProxy.writeDefaultConfig({ activated :true });
|
||||
}
|
||||
|
||||
@@ -224,7 +223,6 @@ async function getLogs(unit, options) {
|
||||
|
||||
let logFile = '';
|
||||
if (unit === 'box') logFile = path.join(paths.LOG_DIR, 'box.log'); // box.log is at the top
|
||||
else if (unit.startsWith('crash-')) logFile = path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log');
|
||||
else throw new BoxError(BoxError.BAD_FIELD, `No such unit '${unit}'`);
|
||||
|
||||
const cp = logs.tail([logFile], { lines: options.lines, follow: options.follow });
|
||||
@@ -337,21 +335,13 @@ async function updateDiskUsage() {
|
||||
}
|
||||
|
||||
async function getBlockDevices() {
|
||||
let info;
|
||||
const info = safe.JSON.parse(safe.child_process.execSync('lsblk --paths --json --list --fs', { encoding: 'utf8' }));
|
||||
if (!info) throw new BoxError(BoxError.INTERNAL_ERROR, safe.error.message);
|
||||
|
||||
try {
|
||||
info = JSON.parse(execSync('lsblk --paths --json --list --fs', { encoding: 'utf8' }));
|
||||
} catch (e) {
|
||||
console.error('Failed to list disks:', e);
|
||||
throw new BoxError(BoxError.INTERNAL_ERROR, e);
|
||||
}
|
||||
|
||||
// filter only for ext4 and xfs disks
|
||||
const devices = info.blockdevices.filter(d => d.fstype === 'ext4' || d.fstype === 'xfs');
|
||||
|
||||
debug(`getBlockDevices: Found ${devices.length} devices. ${devices.map(d => d.name).join(', ')}`);
|
||||
|
||||
// convert to fixed format
|
||||
return devices.map(function (d) {
|
||||
return {
|
||||
path: d.name,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
sendFailureLogs
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
AuditSource = require('./auditsource.js'),
|
||||
child_process = require('child_process'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
util = require('util');
|
||||
|
||||
const COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
|
||||
|
||||
const CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
|
||||
const CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
|
||||
|
||||
async function collectLogs(unitName) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
|
||||
const logs = child_process.execSync(`sudo ${COLLECT_LOGS_CMD} ${unitName}`, { encoding: 'utf8' });
|
||||
return logs;
|
||||
}
|
||||
|
||||
async function sendFailureLogs(unitName) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
|
||||
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
|
||||
const timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
|
||||
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
|
||||
console.log('Crash log already sent within window');
|
||||
return;
|
||||
}
|
||||
|
||||
let [error, logs] = await safe(collectLogs(unitName));
|
||||
if (error) {
|
||||
console.error('Failed to collect logs.', error);
|
||||
logs = util.format('Failed to collect logs.', error);
|
||||
}
|
||||
|
||||
const crashId = `${new Date().toISOString()}`;
|
||||
console.log(`Creating crash log for ${unitName} with id ${crashId}`);
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.CRASH_LOG_DIR, `${crashId}.log`), logs)) console.log(`Failed to stash logs to ${crashId}.log:`, safe.error);
|
||||
|
||||
[error] = await safe(eventlog.add(eventlog.ACTION_PROCESS_CRASH, AuditSource.HEALTH_MONITOR, { processName: unitName, crashId: crashId }));
|
||||
if (error) console.log(`Error sending crashlog. Logs stashed at ${crashId}.log`);
|
||||
|
||||
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = delay;
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
function delay(msecs) {
|
||||
assert.strictEqual(typeof msecs, 'number');
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, msecs);
|
||||
});
|
||||
}
|
||||
+2
-3
@@ -7,8 +7,7 @@ exports = module.exports = {
|
||||
const assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
dns = require('dns'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
safe = require('safetydance');
|
||||
|
||||
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
|
||||
// are added for DNS server software to enclose spaces. Such quotes may also be returned
|
||||
@@ -20,7 +19,7 @@ async function resolve(hostname, rrtype, options) {
|
||||
|
||||
const defaultOptions = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
|
||||
const resolver = new dns.promises.Resolver();
|
||||
options = _.extend({ }, defaultOptions, options);
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
|
||||
// Only use unbound on a Cloudron
|
||||
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
|
||||
|
||||
+2
-1
@@ -242,7 +242,8 @@ async function verifyDomainConfig(domainObject) {
|
||||
if (error || !nameservers) throw new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers');
|
||||
|
||||
// https://docs.hetzner.com/dns-console/dns/general/dns-overview#the-hetzner-online-name-servers-are
|
||||
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('oxygen.ns.hetzner.com') === -1) {
|
||||
const nsMap = nameservers.map(function (n) { return n.toLowerCase(); });
|
||||
if (!nsMap.includes('oxygen.ns.hetzner.com') && !nsMap.includes('ns1.your-server.de')) {
|
||||
debug('verifyDomainConfig: %j does not contain Hetzner NS', nameservers);
|
||||
throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Hetzner');
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ exports = module.exports = {
|
||||
verifyDomainConfig
|
||||
};
|
||||
|
||||
// https://github.com/aws/aws-sdk-js/issues/4354
|
||||
require('aws-sdk/lib/maintenance_mode_message').suppress = true;
|
||||
|
||||
const assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
|
||||
+8
-7
@@ -37,7 +37,6 @@ const apps = require('./apps.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:docker'),
|
||||
delay = require('./delay.js'),
|
||||
Docker = require('dockerode'),
|
||||
paths = require('./paths.js'),
|
||||
promiseRetry = require('./promise-retry.js'),
|
||||
@@ -46,8 +45,8 @@ const apps = require('./apps.js'),
|
||||
shell = require('./shell.js'),
|
||||
safe = require('safetydance'),
|
||||
system = require('./system.js'),
|
||||
volumes = require('./volumes.js'),
|
||||
_ = require('underscore');
|
||||
timers = require('timers/promises'),
|
||||
volumes = require('./volumes.js');
|
||||
|
||||
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
|
||||
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
|
||||
@@ -278,6 +277,8 @@ async function createSubcontainer(app, name, cmd, options) {
|
||||
`CLOUDRON_APP_DOMAIN=${domain}`
|
||||
];
|
||||
|
||||
if (app.manifest.multiDomain) stdEnv.push(`CLOUDRON_ALIAS_DOMAINS=${app.aliasDomains.map(ad => ad.fqdn).join(',')}`);
|
||||
|
||||
const secondaryDomainsEnv = app.secondaryDomains.map(sd => `${sd.environmentVariable}=${sd.fqdn}`);
|
||||
|
||||
const portEnv = [];
|
||||
@@ -318,7 +319,7 @@ async function createSubcontainer(app, name, cmd, options) {
|
||||
app.manifest.runtimeDirs.forEach(dir => runtimeVolumes[dir] = {});
|
||||
}
|
||||
|
||||
let containerOptions = {
|
||||
const containerOptions = {
|
||||
name: name, // for referencing containers
|
||||
Tty: isAppContainer,
|
||||
Image: app.manifest.dockerImage,
|
||||
@@ -405,9 +406,9 @@ async function createSubcontainer(app, name, cmd, options) {
|
||||
];
|
||||
}
|
||||
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
const mergedOptions = Object.assign({}, containerOptions, options);
|
||||
|
||||
const [createError, container] = await safe(gConnection.createContainer(containerOptions));
|
||||
const [createError, container] = await safe(gConnection.createContainer(mergedOptions));
|
||||
if (createError && createError.statusCode === 409) throw new BoxError(BoxError.ALREADY_EXISTS, createError);
|
||||
if (createError) throw new BoxError(BoxError.DOCKER_ERROR, createError);
|
||||
|
||||
@@ -648,7 +649,7 @@ async function update(name, memory, memorySwap) {
|
||||
for (let times = 0; times < 10; times++) {
|
||||
const [error] = await safe(shell.promises.spawn(`update(${name})`, '/usr/bin/docker', args, { }));
|
||||
if (!error) return;
|
||||
await delay(60 * 1000);
|
||||
await timers.setTimeout(60 * 1000);
|
||||
}
|
||||
|
||||
throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to update container');
|
||||
|
||||
+2
-3
@@ -17,8 +17,7 @@ const apps = require('./apps.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
util = require('util');
|
||||
|
||||
let gHttpServer = null;
|
||||
|
||||
@@ -67,7 +66,7 @@ function attachDockerRequest(req, res, next) {
|
||||
function containersCreate(req, res, next) {
|
||||
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
|
||||
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
|
||||
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
|
||||
safe.set(req.body, 'Labels', Object.assign({}, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
|
||||
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
|
||||
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ exports = module.exports = {
|
||||
ACTION_SUPPORT_TICKET: 'support.ticket',
|
||||
ACTION_SUPPORT_SSH: 'support.ssh',
|
||||
|
||||
ACTION_PROCESS_CRASH: 'system.crash'
|
||||
ACTION_PROCESS_CRASH: 'system.crash' // obsolete
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ async function getClient(externalLdapConfig, options) {
|
||||
|
||||
// ensure we don't just crash
|
||||
client.on('error', function (error) {
|
||||
console.error('ExternalLdap client error:', error);
|
||||
debug('getClient: ExternalLdap client error:', error);
|
||||
reject(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
});
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ exports = module.exports = {
|
||||
'images': {
|
||||
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.5.0@sha256:c59a6da9ea55073ede1ba6329739fca72eddf64c3a3c10280bcc5b7fb8197865' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.3.7@sha256:5c8fe784859a5bc8c839712d8b52427247a54bce9126fb2d50ca2535e6330647' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:5.0.7@sha256:4be3401b9d1374d1e165bdbd1a49ea8cdee748f15f180538306637868abffbac' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:5.0.6@sha256:94bc17f8e9daf8de01c9676bc6c9ac4d791cc10b1a602d9647a8f545ea5568fc' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.3.7@sha256:6217723c33f1555fdaf5064a4ee87ab582523ac24fe15fafe9838b137e185296' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.4.5@sha256:83dad2697791f358be75e2fc686840800b26aafc697b1250e8457c97ece67a47' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.8.3@sha256:877a1afb99e8cae8c82d5a2fca77840425eb7fafc24360fdd1c9c299e41bcfeb' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.5.0@sha256:ee6da2599a72afaec1d80c41db9b5fe79c882fb920195659e871501ea2e94d18' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.8.6@sha256:c88fc3502828dc3c15f39b10e2b949a447a682a686854ac358a8983ac0999ed3' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:3.3.0@sha256:005addac7e7576f3960b562404ce59442bc861626af0ae0f5122484f5bfcbbc1' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.7.2@sha256:a6f81d4dbbb90f6d57d30722f860d431cdba67c3500fb327878d29c6bb6357d2' }
|
||||
}
|
||||
|
||||
+1
-1
@@ -529,7 +529,7 @@ async function checkRblStatus(domain) {
|
||||
|
||||
debug(`checkRblStatus: ${domain} (flippedIp: ${flippedIp}) is in the blacklist of ${JSON.stringify(rblServer)}`);
|
||||
|
||||
const result = _.extend({ }, rblServer);
|
||||
const result = Object.assign({}, rblServer);
|
||||
|
||||
const [error2, txtRecords] = await safe(dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS));
|
||||
result.txtRecords = error2 || !txtRecords ? 'No txt record' : txtRecords.map(x => x.join(''));
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
getBlocklist,
|
||||
setBlocklist
|
||||
setBlocklist,
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
|
||||
@@ -269,9 +269,20 @@ server {
|
||||
# client_max_body_size 1m;
|
||||
# }
|
||||
|
||||
location @dashboarderrorredirect {
|
||||
return 302 /;
|
||||
}
|
||||
|
||||
location / {
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
index index.html index.htm;
|
||||
error_page 404 = @dashboarderrorredirect;
|
||||
}
|
||||
|
||||
# Cross domain translation access for local development and login page
|
||||
location ~ ^/translation/ {
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
add_header "Access-Control-Allow-Origin" "*";
|
||||
}
|
||||
|
||||
# Cross domain webfont access for proxy auth login page https://github.com/h5bp/server-configs/issues/85
|
||||
|
||||
+130
-59
@@ -31,6 +31,8 @@ const assert = require('assert'),
|
||||
jose = require('jose'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
tokens = require('./tokens.js'),
|
||||
translation = require('./translation.js'),
|
||||
url = require('url'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
@@ -41,6 +43,9 @@ const OIDC_CLIENTS_FIELDS = [ 'id', 'secret', 'name', 'appId', 'loginRedirectUri
|
||||
const ROUTE_PREFIX = '/openid';
|
||||
const DEFAULT_TOKEN_SIGNATURE_ALGORITHM='RS256';
|
||||
|
||||
const DASHBOARD_CLIENT_ID = 'dashboard';
|
||||
const DEV_CLIENT_ID = 'development';
|
||||
|
||||
let gHttpServer = null;
|
||||
|
||||
// -----------------------------
|
||||
@@ -63,8 +68,6 @@ async function clientsAdd(id, data) {
|
||||
assert.strictEqual(typeof data.appId, 'string');
|
||||
assert(data.tokenSignatureAlgorithm === 'RS256' || data.tokenSignatureAlgorithm === 'EdDSA');
|
||||
|
||||
debug(`clientsAdd: id:${id} secret:${data.secret} name:${data.name} appId:${data.appId} loginRedirectUri:${data.loginRedirectUri} logoutRedirectUri:${data.logoutRedirectUri} tokenSignatureAlgorithm:${data.tokenSignatureAlgorithm}`);
|
||||
|
||||
const query = `INSERT INTO ${OIDC_CLIENTS_TABLE_NAME} (id, secret, name, appId, loginRedirectUri, logoutRedirectUri, tokenSignatureAlgorithm) VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
||||
const args = [ id, data.secret, data.name, data.appId, data.loginRedirectUri, data.logoutRedirectUri, data.tokenSignatureAlgorithm ];
|
||||
|
||||
@@ -76,7 +79,25 @@ async function clientsAdd(id, data) {
|
||||
async function clientsGet(id) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
|
||||
debug(`clientsGet: id:${id}`);
|
||||
if (id === DASHBOARD_CLIENT_ID) {
|
||||
return {
|
||||
id: DASHBOARD_CLIENT_ID,
|
||||
secret: 'notused',
|
||||
application_type: 'web',
|
||||
response_types: ['code', 'code token'],
|
||||
grant_types: ['authorization_code', 'implicit'],
|
||||
loginRedirectUri: settings.dashboardOrigin() + '/authcallback.html'
|
||||
};
|
||||
} else if (id === DEV_CLIENT_ID) {
|
||||
return {
|
||||
id: DEV_CLIENT_ID,
|
||||
secret: 'notused',
|
||||
application_type: 'native', // have to use native here to support plaintext http, this however makes it impossible to skip consent screen
|
||||
response_types: ['code', 'code token'],
|
||||
grant_types: ['authorization_code', 'implicit'],
|
||||
loginRedirectUri: 'http://localhost:4000/authcallback.html'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await database.query(`SELECT ${OIDC_CLIENTS_FIELDS} FROM ${OIDC_CLIENTS_TABLE_NAME} WHERE id = ?`, [ id ]);
|
||||
if (result.length === 0) return null;
|
||||
@@ -93,8 +114,6 @@ async function clientsUpdate(id, data) {
|
||||
assert.strictEqual(typeof data.appId, 'string');
|
||||
assert(data.tokenSignatureAlgorithm === 'RS256' || data.tokenSignatureAlgorithm === 'EdDSA');
|
||||
|
||||
debug(`clientsUpdate: id:${id} secret:${data.secret} name:${data.name} appId:${data.appId} loginRedirectUri:${data.loginRedirectUri} logoutRedirectUri:${data.logoutRedirectUri} tokenSignatureAlgorithm:${data.tokenSignatureAlgorithm}`);
|
||||
|
||||
const result = await database.query(`UPDATE ${OIDC_CLIENTS_TABLE_NAME} SET secret=?, name=?, appId=?, loginRedirectUri=?, logoutRedirectUri=?, tokenSignatureAlgorithm=? WHERE id = ?`, [ data.secret, data.name, data.appId, data.loginRedirectUri, data.logoutRedirectUri, data.tokenSignatureAlgorithm, id]);
|
||||
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'client not found');
|
||||
}
|
||||
@@ -131,7 +150,8 @@ function load(modelName) {
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(filePath), 'utf8');
|
||||
} catch (e) {
|
||||
debug(`load: failed to read ${filePath}, start with new one. %o`, e);
|
||||
if (e.code === 'ENOENT') debug(`load: failed to read ${filePath}, start with new one.`);
|
||||
else debug(`load: failed to read ${filePath}, use in-memory. %o`, e);
|
||||
}
|
||||
|
||||
DATA_STORE[modelName] = data;
|
||||
@@ -146,9 +166,9 @@ function save(modelName) {
|
||||
debug(`save: model ${modelName} to ${filePath}.`);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(DATA_STORE[modelName]), 'utf8');
|
||||
fs.writeFileSync(filePath, JSON.stringify(DATA_STORE[modelName], null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
debug(`revokeByUserId: failed to write ${filePath}`, e);
|
||||
debug(`save: failed to write ${filePath}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +179,6 @@ function save(modelName) {
|
||||
async function revokeByUserId(userId) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
|
||||
debug(`revokeByUserId: userId:${userId}`);
|
||||
|
||||
function revokeObjects(modelName) {
|
||||
load(modelName);
|
||||
|
||||
@@ -195,9 +213,11 @@ class CloudronAdapter {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
|
||||
debug(`Creating storage adapter for ${name}`);
|
||||
debug(`Creating OpenID storage adapter for ${name}`);
|
||||
|
||||
if (this.name !== 'Client') {
|
||||
if (this.name === 'Client') {
|
||||
return;
|
||||
} else {
|
||||
load(name);
|
||||
}
|
||||
}
|
||||
@@ -215,10 +235,19 @@ class CloudronAdapter {
|
||||
*
|
||||
*/
|
||||
async upsert(id, payload, expiresIn) {
|
||||
debug(`[${this.name}] upsert id:${id} expiresIn:${expiresIn}`, payload);
|
||||
|
||||
if (this.name === 'Client') {
|
||||
console.log('WARNING!! this should not happen as it is stored in our db');
|
||||
debug('upsert: this should not happen as it is stored in our db');
|
||||
} else if (this.name === 'AccessToken' && (payload.clientId === DASHBOARD_CLIENT_ID || payload.clientId === DEV_CLIENT_ID)) {
|
||||
const clientId = payload.clientId;
|
||||
const identifier = payload.accountId;
|
||||
const expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS;
|
||||
const accessToken = id;
|
||||
|
||||
const [error] = await safe(tokens.add({ clientId, identifier, expires, accessToken }));
|
||||
if (error) {
|
||||
console.log('Error adding access token', error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
DATA_STORE[this.name][id] = { id, expiresIn, payload, consumed: false };
|
||||
save(this.name);
|
||||
@@ -236,28 +265,27 @@ class CloudronAdapter {
|
||||
*
|
||||
*/
|
||||
async find(id) {
|
||||
debug(`[${this.name}] find id:${id}`);
|
||||
|
||||
if (this.name === 'Client') {
|
||||
const [error, client] = await safe(clientsGet(id));
|
||||
if (error) {
|
||||
console.log('Error getting client', error);
|
||||
debug('find: error getting client', error);
|
||||
return null;
|
||||
}
|
||||
if (!client) return null;
|
||||
|
||||
debug(`[${this.name}] find id:${id}`, client);
|
||||
|
||||
const tmp = {};
|
||||
tmp.application_type = 'native'; // default is web but we want more flexible redirectUris and this is only used in https://github.com/panva/node-oidc-provider/blob/03c9bc513860e68ee7be84f99bfc9dc930b224e8/lib/helpers/client_schema.js#L53
|
||||
tmp.application_type = client.application_type || 'native'; // default is web but we want more flexible redirectUris and this is only used in https://github.com/panva/node-oidc-provider/blob/03c9bc513860e68ee7be84f99bfc9dc930b224e8/lib/helpers/client_schema.js#L536
|
||||
tmp.client_id = id;
|
||||
tmp.client_secret = client.secret;
|
||||
tmp.id_token_signed_response_alg = client.tokenSignatureAlgorithm || 'RS256';
|
||||
|
||||
if (client.response_types) tmp.response_types = client.response_types;
|
||||
if (client.grant_types) tmp.grant_types = client.grant_types;
|
||||
|
||||
if (client.appId) {
|
||||
const [error, app] = await safe(apps.get(client.appId));
|
||||
if (error || !app) {
|
||||
console.error(`oidc: Unkown app for client with appId ${client.appId}`);
|
||||
debug(`find: Unknown app for client with appId ${client.appId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -272,12 +300,26 @@ class CloudronAdapter {
|
||||
if (client.logoutRedirectUri) tmp.post_logout_redirect_uris = [ client.logoutRedirectUri ];
|
||||
}
|
||||
|
||||
return tmp;
|
||||
} else if (this.name === 'AccessToken') {
|
||||
debug('find: we dont support finding AccessTokens', id);
|
||||
const [error, result] = await safe(tokens.getByAccessToken(id));
|
||||
if (error || !result) {
|
||||
debug(`find: Unknown accessToken for id ${id} maybe oidc internal?`);
|
||||
|
||||
if (!DATA_STORE[this.name][id]) return null;
|
||||
return DATA_STORE[this.name][id].payload;
|
||||
}
|
||||
|
||||
const tmp = {
|
||||
accountId: result.identifier,
|
||||
clientId: result.clientId
|
||||
};
|
||||
|
||||
return tmp;
|
||||
} else {
|
||||
if (!DATA_STORE[this.name][id]) return null;
|
||||
|
||||
debug(`[${this.name}] find id:${id}`, DATA_STORE[this.name][id]);
|
||||
|
||||
return DATA_STORE[this.name][id].payload;
|
||||
}
|
||||
}
|
||||
@@ -308,10 +350,8 @@ class CloudronAdapter {
|
||||
*
|
||||
*/
|
||||
async findByUid(uid) {
|
||||
debug(`[${this.name}] findByUid uid:${uid}`);
|
||||
|
||||
if (this.name === 'Client') {
|
||||
console.log('WARNING!! this should not happen as it is stored in our db');
|
||||
if (this.name === 'Client' || this.name === 'AccessToken') {
|
||||
debug('findByUid: this should not happen as it is stored in our db');
|
||||
} else {
|
||||
for (let d in DATA_STORE[this.name]) {
|
||||
if (DATA_STORE[this.name][d].payload.uid === uid) return DATA_STORE[this.name][d].payload;
|
||||
@@ -333,10 +373,10 @@ class CloudronAdapter {
|
||||
*
|
||||
*/
|
||||
async consume(id) {
|
||||
debug(`[${this.name}] consume id:${id}`);
|
||||
debug(`[${this.name}] consume: ${id}`);
|
||||
|
||||
if (this.name === 'Client') {
|
||||
console.log('WARNING!! this should not happen as it is stored in our db');
|
||||
debug('consume: this should not happen as it is stored in our db');
|
||||
} else {
|
||||
if (DATA_STORE[this.name][id]) DATA_STORE[this.name][id].consumed = true;
|
||||
save(this.name);
|
||||
@@ -354,10 +394,10 @@ class CloudronAdapter {
|
||||
*
|
||||
*/
|
||||
async destroy(id) {
|
||||
debug(`[${this.name}] destroy id:${id}`);
|
||||
debug(`[${this.name}] destroy: ${id}`);
|
||||
|
||||
if (this.name === 'Client') {
|
||||
console.log('WARNING!! this should not happen as it is stored in our db');
|
||||
debug('destroy: this should not happen as it is stored in our db');
|
||||
} else {
|
||||
delete DATA_STORE[this.name][id];
|
||||
save(this.name);
|
||||
@@ -375,10 +415,10 @@ class CloudronAdapter {
|
||||
*
|
||||
*/
|
||||
async revokeByGrantId(grantId) {
|
||||
debug(`[${this.name}] revokeByGrantId grantId:${grantId}`);
|
||||
debug(`[${this.name}] revokeByGrantId: ${grantId}`);
|
||||
|
||||
if (this.name === 'Client') {
|
||||
console.log('WARNING!! this should not happen as it is stored in our db');
|
||||
debug('revokeByGrantId: this should not happen as it is stored in our db');
|
||||
} else {
|
||||
for (let d in DATA_STORE[this.name]) {
|
||||
if (DATA_STORE[this.name][d].grantId === grantId) {
|
||||
@@ -397,33 +437,48 @@ function renderInteractionPage(provider) {
|
||||
assert.strictEqual(typeof provider, 'object');
|
||||
|
||||
return async function (req, res, next) {
|
||||
const translationAssets = await translation.getTranslations();
|
||||
|
||||
try {
|
||||
const { uid, prompt, params, session } = await provider.interactionDetails(req, res);
|
||||
|
||||
debug(`route interaction get uid:${uid} prompt.name:${prompt.name} client_id:${params.client_id} session:${session}`);
|
||||
|
||||
const client = await clientsGet(params.client_id);
|
||||
|
||||
let app = null;
|
||||
if (client.appId) app = await apps.get(client.appId);
|
||||
|
||||
switch (prompt.name) {
|
||||
case 'login': {
|
||||
return res.render('login', {
|
||||
const options = {
|
||||
submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/login`,
|
||||
iconUrl: '/api/v1/cloudron/avatar',
|
||||
name: client?.name || 'Cloudron'
|
||||
});
|
||||
};
|
||||
|
||||
if (app) {
|
||||
options.name = app.label || app.fqdn;
|
||||
options.iconUrl = app.iconUrl;
|
||||
}
|
||||
|
||||
const template = fs.readFileSync(__dirname + '/oidc_templates/login.ejs', 'utf-8');
|
||||
const html = ejs.render(translation.translate(template, translationAssets.translations || {}, translationAssets.fallback || {}), options);
|
||||
|
||||
return res.send(html);
|
||||
}
|
||||
case 'consent': {
|
||||
const options = {
|
||||
hasAccess: false,
|
||||
submitUrl: '',
|
||||
iconUrl: '/api/v1/cloudron/avatar',
|
||||
name: client?.name || ''
|
||||
};
|
||||
|
||||
// check if user has access to the app if client refers to an app
|
||||
if (client.appId) {
|
||||
const app = await apps.get(client.appId);
|
||||
if (app) {
|
||||
const user = await users.get(session.accountId);
|
||||
|
||||
options.name = app.label || app.fqdn;
|
||||
options.iconUrl = app.iconUrl;
|
||||
options.hasAccess = apps.canAccess(app, user);
|
||||
} else {
|
||||
options.hasAccess = true;
|
||||
@@ -437,10 +492,8 @@ function renderInteractionPage(provider) {
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
debug('route interaction get error');
|
||||
console.log(error);
|
||||
|
||||
return next(error);
|
||||
debug('route interaction get error', error);
|
||||
return res.render('error', { errorMessage: error.error_description || 'Internal error' });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -452,12 +505,9 @@ function interactionLogin(provider) {
|
||||
const [detailsError, details] = await safe(provider.interactionDetails(req, res));
|
||||
if (detailsError) return next(new HttpError(500, detailsError));
|
||||
|
||||
const uid = details.uid;
|
||||
const prompt = details.prompt;
|
||||
const name = prompt.name;
|
||||
|
||||
debug(`route interaction login post uid:${uid} prompt.name:${name}`);
|
||||
|
||||
assert.equal(name, 'login');
|
||||
|
||||
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'A username must be non-empty string'));
|
||||
@@ -470,9 +520,9 @@ function interactionLogin(provider) {
|
||||
|
||||
const [verifyError, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN, { totpToken }));
|
||||
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, verifyError.message));
|
||||
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
|
||||
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Username and password does not match'));
|
||||
if (verifyError) return next(new HttpError(500, verifyError));
|
||||
if (!user) return next(new HttpError(401, 'Unauthorized'));
|
||||
if (!user) return next(new HttpError(401, 'Username and password does not match'));
|
||||
|
||||
// TODO we may have to check what else the Account class provides, in which case we have to map those things
|
||||
const result = {
|
||||
@@ -564,8 +614,6 @@ function interactionAbort(provider) {
|
||||
assert.strictEqual(typeof provider, 'object');
|
||||
|
||||
return async function (req, res, next) {
|
||||
debug('route interaction abort');
|
||||
|
||||
try {
|
||||
const result = {
|
||||
error: 'access_denied',
|
||||
@@ -587,8 +635,6 @@ function interactionAbort(provider) {
|
||||
* or not return them in id tokens but only userinfo and so on.
|
||||
*/
|
||||
async function claims(userId, use, scope) {
|
||||
debug(`claims: userId:${userId} use:${use} scope:${scope}`);
|
||||
|
||||
const [error, user] = await safe(users.get(userId));
|
||||
if (error) return { error: 'user not found' };
|
||||
|
||||
@@ -608,8 +654,6 @@ async function claims(userId, use, scope) {
|
||||
preferred_username: user.username
|
||||
};
|
||||
|
||||
debug(`claims: userId:${userId} result`, claims);
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
@@ -623,6 +667,7 @@ async function logoutSource(ctx, form) {
|
||||
ctx.body = ejs.render(fs.readFileSync(path.join(__dirname, 'oidc_templates/logout.ejs'), 'utf8'), data, {});
|
||||
}
|
||||
|
||||
// this is called if client has not specified a post_logout_redirect_uri
|
||||
async function postLogoutSuccessSource(ctx) {
|
||||
// const client = ctx.oidc.client || {}; // client is defined if the user chose to stay logged in with the OP
|
||||
const data = {
|
||||
@@ -633,8 +678,6 @@ async function postLogoutSuccessSource(ctx) {
|
||||
}
|
||||
|
||||
async function findAccount(ctx, id) {
|
||||
debug(`findAccount id:${id}`);
|
||||
|
||||
return {
|
||||
accountId: id,
|
||||
async claims(use, scope) { return await claims(id, use, scope); },
|
||||
@@ -658,7 +701,7 @@ async function start() {
|
||||
|
||||
gHttpServer = http.createServer(app);
|
||||
|
||||
const { Provider } = await import('oidc-provider');
|
||||
const Provider = (await import('oidc-provider')).default;
|
||||
|
||||
// TODO we may want to rotate those in the future
|
||||
const jwksKeys = [];
|
||||
@@ -711,6 +754,12 @@ async function start() {
|
||||
postLogoutSuccessSource
|
||||
},
|
||||
},
|
||||
responseTypes: [
|
||||
'code',
|
||||
'id_token', 'id_token token',
|
||||
'code id_token', 'code token', 'code id_token token',
|
||||
'none',
|
||||
],
|
||||
// if a client only has one redirect uri specified, the client does not have to provide it in the request
|
||||
allowOmittingSingleRegisteredRedirectUri: true,
|
||||
clients: [],
|
||||
@@ -719,10 +768,31 @@ async function start() {
|
||||
keys: [ 'cookiesecret1', 'cookiesecret2' ]
|
||||
},
|
||||
pkce: {
|
||||
required: function pkceRequired(ctx, client) {
|
||||
required: function pkceRequired(/*ctx, client*/) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
conformIdTokenClaims: false,
|
||||
// https://github.com/panva/node-oidc-provider/blob/main/recipes/skip_consent.md
|
||||
loadExistingGrant: async function (ctx) {
|
||||
const grantId = ctx.oidc.result?.consent?.grantId
|
||||
|| ctx.oidc.session.grantIdFor(ctx.oidc.client.clientId);
|
||||
|
||||
if (grantId) {
|
||||
return await ctx.oidc.provider.Grant.find(grantId);
|
||||
} else if (ctx.oidc.client.clientId === DASHBOARD_CLIENT_ID || ctx.oidc.client.clientId === DEV_CLIENT_ID) {
|
||||
const grant = new ctx.oidc.provider.Grant({
|
||||
clientId: ctx.oidc.client.clientId,
|
||||
accountId: ctx.oidc.session.accountId,
|
||||
});
|
||||
|
||||
grant.addOIDCScope('openid email profile');
|
||||
// grant.addOIDCClaims(['first_name']);
|
||||
await grant.save();
|
||||
|
||||
return grant;
|
||||
}
|
||||
},
|
||||
ttl: {
|
||||
// in seconds, can also be a function returning the seconds https://github.com/panva/node-oidc-provider/blob/b1c1a9318036c2d3793cc9e668f99937c5c36bc6/docs/README.md#ttl
|
||||
AccessToken: 3600, // 1 hour
|
||||
@@ -754,6 +824,7 @@ async function start() {
|
||||
app.get (`${ROUTE_PREFIX}/interaction/:uid/abort`, setNoCache, interactionAbort(provider));
|
||||
|
||||
app.use(ROUTE_PREFIX, provider.callback());
|
||||
app.use(middleware.lastMile());
|
||||
|
||||
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.OIDC_PORT, '127.0.0.1');
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
<title>OpenID Connect Error</title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
@@ -53,7 +57,7 @@
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" class="avatar" src="/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h1>OpenID Connect Error</h1>
|
||||
<h2>OpenID Connect Error</h2>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
<title>Authorize <%= name %></title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
@@ -46,35 +50,39 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<% if (hasAccess) { -%>
|
||||
<form id="submitForm" method="post" action="<%= submitUrl %>">
|
||||
<!-- <button type="submit"></button> -->
|
||||
</form>
|
||||
<% } else { -%>
|
||||
<div class="layout-root">
|
||||
<div class="layout-content">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" class="avatar" src="/api/v1/cloudron/avatar"/>
|
||||
<img width="128" height="128" class="avatar" src="<%= iconUrl %>"/>
|
||||
<br/>
|
||||
<% if (hasAccess) { -%>
|
||||
<h3>Authorize for <b><%= name %></b></h3>
|
||||
<% } else { -%>
|
||||
<h3>You do not have access to <b><%= name %></b></h3>
|
||||
<% } -%>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<% if (hasAccess) { -%>
|
||||
<form method="post" action="<%= submitUrl %>">
|
||||
<button class="btn btn-primary btn-outline" type="submit">Authorize</button>
|
||||
</form>
|
||||
<% } else { -%>
|
||||
<a class="btn btn-primary btn-outline" href="<%= submitUrl %>">Continue</a>
|
||||
<% } -%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } -%>
|
||||
|
||||
<script>
|
||||
|
||||
<% if (hasAccess) { -%>
|
||||
document.getElementById('submitForm').submit();
|
||||
<% } -%>
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Login to <%= name %></title>
|
||||
<title>{{ login.loginTo }} <%= name %></title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
@@ -16,6 +20,8 @@
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/3rdparty/js/password-reveal.js"></script>
|
||||
|
||||
<style>
|
||||
|
||||
.card {
|
||||
@@ -51,9 +57,9 @@
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" class="avatar" src="/api/v1/cloudron/avatar"/>
|
||||
<img width="128" height="128" class="avatar" src="<%= iconUrl %>"/>
|
||||
<br/>
|
||||
<h1>Login to <%= name %></h1>
|
||||
<h1><small>{{ login.loginTo }}</small> <%= name %></h1>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -61,18 +67,23 @@
|
||||
<div class="col-md-12">
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">Username</label>
|
||||
<label class="control-label" for="inputUsername">{{ login.username }}</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<label class="control-label" for="inputPassword">{{ login.password }}</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" required password-reveal>
|
||||
<p class="has-error" id="passwordError"></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputTotpToken">2FA Token</label>
|
||||
<label class="control-label" for="inputTotpToken">{{ login.2faToken }}</label>
|
||||
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" value="">
|
||||
<p class="has-error" id="totpError"></p>
|
||||
</div>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/passwordreset.html">{{ login.resetPasswordAction }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit">{{ login.signInAction }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit">Log in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,6 +96,9 @@
|
||||
document.getElementById('loginForm').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
document.getElementById('passwordError').innerText = '';
|
||||
document.getElementById('totpError').innerText = '';
|
||||
|
||||
var apiUrl = '<%= submitUrl %>';
|
||||
console.log('submit', apiUrl);
|
||||
|
||||
@@ -94,19 +108,36 @@ document.getElementById('loginForm').addEventListener('submit', function (event)
|
||||
totpToken: document.getElementById('inputTotpToken').value
|
||||
};
|
||||
|
||||
let res;
|
||||
fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: { 'Content-type': 'application/json; charset=UTF-8' }
|
||||
}).then(function (response) {
|
||||
if (response.ok) return response.json();
|
||||
return Promise.reject(response);
|
||||
res = response;
|
||||
return res.json(); // we always return objects
|
||||
}).then(function (data) {
|
||||
if (res.status === 401) {
|
||||
if (data.message === 'Username and password does not match') {
|
||||
document.getElementById('inputPassword').value = '';
|
||||
document.getElementById('inputPassword').focus();
|
||||
document.getElementById('passwordError').innerText = '{{ login.errorIncorrectCredentials }}';
|
||||
} else if (data.message.indexOf('totpToken') !== -1) {
|
||||
document.getElementById('inputTotpToken').value = '';
|
||||
document.getElementById('inputTotpToken').focus();
|
||||
document.getElementById('totpError').innerText = '{{ login.errorIncorrect2FAToken }}';
|
||||
} else {
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
} else if (res.status !== 200) {
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
|
||||
if (data.redirectTo) window.location.href = data.redirectTo;
|
||||
else console.log('login success but missing redirectTo in data:', data);
|
||||
}).catch(function (error) {
|
||||
if (error.status === 401) document.getElementById('inputPassword').value = ''
|
||||
console.warn('Something went wrong.', error);
|
||||
document.getElementById('passwordError').innerText = '{{ login.errorInternal }}';
|
||||
console.warn(error, res);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
<title>OpenID Logout</title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
<title>Cloudron Logout</title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
@@ -53,13 +57,7 @@
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" class="avatar" src="/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h1>Succesfully logged out</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a href="<%- dashboardOrigin %>" class="btn btn-primary btn-outline">Open dashboard</a>
|
||||
<h2>Succesfully logged out</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -50,13 +50,13 @@ exports = module.exports = {
|
||||
FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'platformdata/firewall/blocklist.txt'),
|
||||
LDAP_ALLOWLIST_FILE: path.join(baseDir(), 'platformdata/firewall/ldap_allowlist.txt'),
|
||||
REVERSE_PROXY_REBUILD_FILE: path.join(baseDir(), 'platformdata/nginx/rebuild-needed'),
|
||||
NGINX_TRUSTED_IPS_FILE: path.join(baseDir(), 'platformdata/nginx/trusted.ips'),
|
||||
|
||||
BOX_DATA_DIR: path.join(baseDir(), 'boxdata/box'),
|
||||
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
|
||||
|
||||
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
|
||||
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
|
||||
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
|
||||
BOX_LOG_FILE: path.join(baseDir(), 'platformdata/logs/box.log'),
|
||||
|
||||
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
|
||||
|
||||
+3
-3
@@ -13,7 +13,6 @@ const apps = require('./apps.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
delay = require('./delay.js'),
|
||||
fs = require('fs'),
|
||||
infra = require('./infra_version.js'),
|
||||
locker = require('./locker.js'),
|
||||
@@ -23,6 +22,7 @@ const apps = require('./apps.js'),
|
||||
services = require('./services.js'),
|
||||
shell = require('./shell.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
timers = require('timers/promises'),
|
||||
volumes = require('./volumes.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -74,7 +74,7 @@ async function start(options) {
|
||||
const retry = error.reason === BoxError.DATABASE_ERROR && (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ECONNREFUSED');
|
||||
debug(`Failed to start services. retry=${retry} (attempt ${attempt}): ${error.message}`);
|
||||
if (!retry) throw error; // refuse to start
|
||||
await delay(10000);
|
||||
await timers.setTimeout(10000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ async function pruneInfraImages() {
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
const parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
const normalizedTag = parts[1].replace('registry.ipv6.docker.com/', '').replace('registry-1.docker.io/', '');
|
||||
const normalizedTag = parts[1].replace('registry.ipv6.docker.com/', '').replace('registry-1.docker.io/', '').replace('registry.docker.com', '');
|
||||
|
||||
if (image.tag === normalizedTag) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports = module.exports = promiseRetry;
|
||||
|
||||
const assert = require('assert'),
|
||||
delay = require('./delay.js'),
|
||||
timers = require('timers/promises'),
|
||||
util = require('util');
|
||||
|
||||
async function promiseRetry(options, asyncFunction) {
|
||||
@@ -19,7 +19,7 @@ async function promiseRetry(options, asyncFunction) {
|
||||
if (i === times - 1) throw error;
|
||||
if (options.retry && !options.retry(error)) throw error; // no more retry
|
||||
if (options.debug) options.debug(`Attempt ${i+1} failed. Will retry: ${error.message}`);
|
||||
await delay(interval);
|
||||
await timers.setTimeout(interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -28,8 +28,7 @@ const assert = require('assert'),
|
||||
paths = require('./paths.js'),
|
||||
users = require('./users.js'),
|
||||
tld = require('tldjs'),
|
||||
tokens = require('./tokens.js'),
|
||||
_ = require('underscore');
|
||||
tokens = require('./tokens.js');
|
||||
|
||||
// we cannot use tasks since the tasks table gets overwritten when db is imported
|
||||
const gProvisionStatus = {
|
||||
@@ -245,7 +244,7 @@ async function getStatus() {
|
||||
|
||||
const allSettings = await settings.list();
|
||||
|
||||
return _.extend({
|
||||
return Object.assign({
|
||||
version: constants.VERSION,
|
||||
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
|
||||
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
|
||||
|
||||
+28
-2
@@ -27,7 +27,10 @@ exports = module.exports = {
|
||||
removeAppConfigs,
|
||||
restoreFallbackCertificates,
|
||||
|
||||
handleCertificateProviderChanged
|
||||
handleCertificateProviderChanged,
|
||||
|
||||
getTrustedIps,
|
||||
setTrustedIps
|
||||
};
|
||||
|
||||
const acme2 = require('./acme2.js'),
|
||||
@@ -52,7 +55,8 @@ const acme2 = require('./acme2.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util');
|
||||
util = require('util'),
|
||||
validator = require('validator');
|
||||
|
||||
const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' });
|
||||
const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh');
|
||||
@@ -728,3 +732,25 @@ async function handleCertificateProviderChanged(domain) {
|
||||
|
||||
safe.fs.appendFileSync(paths.REVERSE_PROXY_REBUILD_FILE, `${domain}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function getTrustedIps() {
|
||||
return await settings.getTrustedIps();
|
||||
}
|
||||
|
||||
async function setTrustedIps(trustedIps) {
|
||||
assert.strictEqual(typeof trustedIps, 'string');
|
||||
|
||||
let trustedIpsConfig = 'real_ip_header X-Forwarded-For;\nreal_ip_recursive on;\n';
|
||||
|
||||
for (const line of trustedIps.split('\n')) {
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const rangeOrIP = line.trim();
|
||||
// this checks for IPv4 and IPv6
|
||||
if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`);
|
||||
trustedIpsConfig += `set_real_ip_from ${rangeOrIP};\n`;
|
||||
}
|
||||
|
||||
await settings.setTrustedIps(trustedIps);
|
||||
if (!safe.fs.writeFileSync(paths.NGINX_TRUSTED_IPS_FILE, trustedIpsConfig, 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
||||
await reload();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
create,
|
||||
cleanup,
|
||||
remount,
|
||||
getMountStatus
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
@@ -63,3 +64,9 @@ async function remount(req, res, next) {
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
async function getMountStatus(req, res, next) {
|
||||
const [error, mountStatus] = await safe(backups.getMountStatus());
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
next(new HttpSuccess(200, mountStatus));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user