Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9470654394 | |||
| 28feadd6c5 | |||
| af3ed04b7f | |||
| 2da99673cd | |||
| 476adcb029 | |||
| b2c8f87276 | |||
| bd4e132709 | |||
| fa8fcf8761 | |||
| 8e92b53d9f | |||
| 6f90bd3db0 | |||
| a261d8b754 | |||
| 9643b7ed1b | |||
| ec191d51bc | |||
| a5452e4b15 | |||
| 8522802f85 | |||
| 6f2e3afe07 | |||
| 70dfb41d95 | |||
| 34f04828c5 | |||
| a78799973d | |||
| 1797148951 | |||
| 67caa89591 | |||
| e3a88e9f5b | |||
| e9910c9b95 | |||
| 45e058bdc1 | |||
| 9af5404921 | |||
| 5c4ca1b699 | |||
| b6827736db | |||
| aada3f3979 | |||
| dc07078fd4 | |||
| ae8278bdb3 | |||
| 286de8cdcb | |||
| ca11d5af94 | |||
| fb04f78112 | |||
| 75fa2dfd67 | |||
| 137267e604 | |||
| 642487f4c5 | |||
| 783ad9ecda | |||
| 0213a368b9 | |||
| f1e7594b79 | |||
| 02fd52e366 | |||
| 2d5e0a51bd | |||
| 1cd82dcd4c | |||
| 5ba30d0236 | |||
| c0ea5c31eb | |||
| adee5fa25f | |||
| f9af84fd85 | |||
| 41cb381a2e | |||
| 50ca07bfb8 | |||
| 07732310c1 | |||
| 854661e2d4 | |||
| 8cac83ed98 | |||
| 5ee8e9da80 | |||
| f5c81f5882 | |||
| a415b70adf |
@@ -3011,3 +3011,28 @@
|
||||
* Give domains list a larger max-height
|
||||
* Make app error compatible with previous releases
|
||||
|
||||
[9.0.4]
|
||||
* filemanager: fix missing translations
|
||||
* display backup duration
|
||||
* add hetznercloud DNS provider
|
||||
|
||||
[9.0.5]
|
||||
* access control/operators: remove deleted users and groups
|
||||
* backupcleaner: fix scoping of cleanup by site id
|
||||
* Use normal buttons for app start/stop
|
||||
* site schedule: Fix hourly display
|
||||
|
||||
[9.0.6]
|
||||
* Autofocus search in appstore view
|
||||
* All settings in sidebar should be same icon
|
||||
* Make backup content list a TableView so we can sort it by size and fileCount
|
||||
* Fix filemanager for custom apps
|
||||
* Sort apps in the grid by label
|
||||
* Filter dropdowns are searchable with more than 10 entries
|
||||
* Show app icons in the grid in grayscale if app is stopped
|
||||
* Support wildcard domain aliases in app location
|
||||
|
||||
[9.0.7]
|
||||
* externalldap: only set group members if they changed
|
||||
* Fix issue where backups remote paths were incorrectly migrated
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
## Translations
|
||||
|
||||
This documents the convention used for the text in the UI.
|
||||
|
||||
### Title Case
|
||||
|
||||
All words are capitalized.
|
||||
|
||||
In title case, articles (a/an/the) ,conjunctions (and/but/or/...) and prepositions (on/at/...)
|
||||
inside a phrase are not capitalized.
|
||||
|
||||
Everything else is capitalized - noun, pronoun, verb, adverb.
|
||||
|
||||
Examples:
|
||||
|
||||
* "Sign In to Your Account"
|
||||
* "Terms and Conditions"
|
||||
* "Getting Started with GraphQL"
|
||||
* "Between You and Me"
|
||||
|
||||
### Sentence case
|
||||
|
||||
Only first word is capitalized.
|
||||
|
||||
### Convention
|
||||
|
||||
| Element | Recommended Style | Example |
|
||||
| -------------- | ---------------------- | -------------------------------- |
|
||||
| Headings | Title Case | Manage Account |
|
||||
| Sub heading | Title Case | Create Admin Account |
|
||||
| Form Labels | Title Case | Email Address |
|
||||
| Buttons | Sentence Case | Save changes |
|
||||
| Radio Buttons | Sentence Case | Option one / Option two |
|
||||
| Menu action | Sentence Case | Select all |
|
||||
| Switches | Sentence Case | Allow users to edit email |
|
||||
| Descriptions | Sentence case | Enter your password to continue. |
|
||||
| Tooltips | Sentence case | Click to edit. |
|
||||
| Error Messages | Sentence case | Password is too short |
|
||||
| Notifications | Sentence case | Settings saved successfully. |
|
||||
| Legend (graph) | Sentence case | Docker volume, Box data. |
|
||||
|
||||
Hints in brackets are small case. Like "(comma separated)".
|
||||
|
||||
## Full stops
|
||||
|
||||
Sentence fragments like form hints and tooltips (which are always visible) do not need a full stop.
|
||||
All other full sentences do.
|
||||
|
||||
description has a full stop unless it's a hint/phrase.
|
||||
|
||||
Checkbox labels do not have a full stop at the end
|
||||
|
||||
Generated
+57
-57
@@ -6,7 +6,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@cloudron/pankow": "^3.5.3",
|
||||
"@cloudron/pankow": "^3.5.6",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
@@ -17,12 +17,12 @@
|
||||
"async": "^3.2.6",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"marked": "^16.4.1",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"vite": "^7.1.10",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
@@ -76,9 +76,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudron/pankow": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.3.tgz",
|
||||
"integrity": "sha512-7N9zHmCEfU86h2O6v8eNc34qrf0AGehf2b/5XlGJrpVsVw06HE+Ty/S/P22ECW+xqZXKu9pwXoal6S+UeQO6LQ==",
|
||||
"version": "3.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.6.tgz",
|
||||
"integrity": "sha512-6N10wpe6iO13fmBrnZ7KMrqRvmyh5e6qCkdj7jaR7ojatp5ozTDCvI1IwY4Nxpj/UWUUgXiYMlNgluWFhN4IsA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
@@ -540,21 +540,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
||||
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0"
|
||||
"@eslint/core": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
|
||||
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
@@ -587,9 +587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
||||
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
||||
"version": "9.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz",
|
||||
"integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -608,12 +608,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
|
||||
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1457,19 +1457,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"version": "9.39.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz",
|
||||
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.1",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@eslint/js": "9.39.0",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
@@ -2383,9 +2383,9 @@
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz",
|
||||
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -2654,9 +2654,9 @@
|
||||
}
|
||||
},
|
||||
"@cloudron/pankow": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.3.tgz",
|
||||
"integrity": "sha512-7N9zHmCEfU86h2O6v8eNc34qrf0AGehf2b/5XlGJrpVsVw06HE+Ty/S/P22ECW+xqZXKu9pwXoal6S+UeQO6LQ==",
|
||||
"version": "3.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.6.tgz",
|
||||
"integrity": "sha512-6N10wpe6iO13fmBrnZ7KMrqRvmyh5e6qCkdj7jaR7ojatp5ozTDCvI1IwY4Nxpj/UWUUgXiYMlNgluWFhN4IsA==",
|
||||
"requires": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
@@ -2845,17 +2845,17 @@
|
||||
}
|
||||
},
|
||||
"@eslint/config-helpers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
||||
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"requires": {
|
||||
"@eslint/core": "^0.16.0"
|
||||
"@eslint/core": "^0.17.0"
|
||||
}
|
||||
},
|
||||
"@eslint/core": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
|
||||
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
}
|
||||
@@ -2877,9 +2877,9 @@
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
||||
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="
|
||||
"version": "9.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz",
|
||||
"integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw=="
|
||||
},
|
||||
"@eslint/object-schema": {
|
||||
"version": "2.1.7",
|
||||
@@ -2887,11 +2887,11 @@
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="
|
||||
},
|
||||
"@eslint/plugin-kit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
|
||||
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"requires": {
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"levn": "^0.4.1"
|
||||
}
|
||||
},
|
||||
@@ -3416,18 +3416,18 @@
|
||||
}
|
||||
},
|
||||
"eslint": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"version": "9.39.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz",
|
||||
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.1",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@eslint/js": "9.39.0",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
@@ -4007,9 +4007,9 @@
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"vite": {
|
||||
"version": "7.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz",
|
||||
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"requires": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@cloudron/pankow": "^3.5.3",
|
||||
"@cloudron/pankow": "^3.5.6",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
@@ -18,12 +18,12 @@
|
||||
"async": "^3.2.6",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"marked": "^16.4.1",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"vite": "^7.1.10",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
|
||||
@@ -733,10 +733,14 @@
|
||||
"reallyDelete": "Sletter du virkelig følgende?"
|
||||
},
|
||||
"newDirectoryDialog": {
|
||||
"title": "Ny mappe"
|
||||
"title": "Ny mappe",
|
||||
"create": "Opret"
|
||||
},
|
||||
"renameDialog": {
|
||||
"reallyOverwrite": "Der findes allerede en fil med det navn. Overskrive eksisterende fil?"
|
||||
"reallyOverwrite": "Der findes allerede en fil med det navn. Overskrive eksisterende fil?",
|
||||
"title": "Omdøb {{ fileName }}",
|
||||
"newName": "Nyt navn",
|
||||
"rename": "Omdøb"
|
||||
},
|
||||
"toolbar": {
|
||||
"new": "Ny",
|
||||
@@ -744,11 +748,80 @@
|
||||
"newFile": "Ny fil",
|
||||
"newFolder": "Ny mappe",
|
||||
"uploadFile": "Upload fil",
|
||||
"restartApp": "Genstart appen"
|
||||
"restartApp": "Genstart appen",
|
||||
"uploadFolder": "Upload mappe",
|
||||
"openTerminal": "Åben terminal",
|
||||
"openLogs": "Vis logs"
|
||||
},
|
||||
"extractionInProgress": "Udvinding i gang",
|
||||
"pasteInProgress": "Indsætning i gang",
|
||||
"deleteInProgress": "Sletning i gang"
|
||||
"deleteInProgress": "Sletning i gang",
|
||||
"chownDialog": {
|
||||
"title": "Ændring af ejerskab",
|
||||
"newOwner": "Ny ejer",
|
||||
"change": "Skift ejer",
|
||||
"recursiveCheckbox": "Ændre ejerskab rekursivt"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Upload af filer ({{ countDone }}/{{{ count }})",
|
||||
"errorAlreadyExists": "Der findes allerede en eller flere filer.",
|
||||
"errorFailed": "Det lykkedes ikke at uploade en eller flere filer. Prøv venligst igen.",
|
||||
"closeWarning": "Du må ikke opdatere siden, før upload er afsluttet.",
|
||||
"retry": "Genoptag",
|
||||
"overwrite": "Overskriv"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Udpakning af {{ fileName }}",
|
||||
"closeWarning": "Du må ikke opdatere siden, før udtrækket er færdigt."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "Filen har ikke gemte ændringer",
|
||||
"details": "Dine ændringer vil gå tabt, hvis du ikke gemmer dem",
|
||||
"dontSave": "Spar ikke"
|
||||
},
|
||||
"notFound": "Ikke fundet",
|
||||
"list": {
|
||||
"name": "Navn",
|
||||
"size": "Størrelse",
|
||||
"owner": "Ejer",
|
||||
"empty": "Ingen filer",
|
||||
"symlink": "symlænk til {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Omdøb",
|
||||
"chown": "Ændring af ejerskab",
|
||||
"extract": "Uddrag her",
|
||||
"download": "Download",
|
||||
"delete": "Slet",
|
||||
"edit": "Rediger",
|
||||
"cut": "Skær",
|
||||
"copy": "Kopier",
|
||||
"paste": "Indsæt",
|
||||
"selectAll": "Vælg alle",
|
||||
"open": "Åben"
|
||||
},
|
||||
"mtime": "Ændret"
|
||||
},
|
||||
"extract": {
|
||||
"error": "Udtrækningen mislykkedes: {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Findes allerede"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Findes allerede"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "genstart af app"
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Uploading",
|
||||
"exitWarning": "Upload er stadig i gang. Skal vi virkelig lukke denne side?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Fortryd",
|
||||
"redo": "Omarbejdning",
|
||||
"save": "Gem"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"incoming": {
|
||||
|
||||
@@ -1031,13 +1031,17 @@
|
||||
},
|
||||
"title": "Datei-Manager",
|
||||
"renameDialog": {
|
||||
"reallyOverwrite": "Eine Datei mit diesem Namen existiert bereits. Diese Datei überschreiben?"
|
||||
"reallyOverwrite": "Eine Datei mit diesem Namen existiert bereits. Diese Datei überschreiben?",
|
||||
"title": "{{ fileName }} umbennen",
|
||||
"newName": "Neuer Name",
|
||||
"rename": "Umbenennen"
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Wirklich löschen?"
|
||||
},
|
||||
"newDirectoryDialog": {
|
||||
"title": "Neuer Ordner"
|
||||
"title": "Neuer Ordner",
|
||||
"create": "Erstellen"
|
||||
},
|
||||
"toolbar": {
|
||||
"newFolder": "Neuer Ordner",
|
||||
@@ -1045,11 +1049,80 @@
|
||||
"upload": "Hochladen",
|
||||
"newFile": "Neue Datei",
|
||||
"uploadFile": "Datei hochladen",
|
||||
"restartApp": "Anwendung neustarten"
|
||||
"restartApp": "Anwendung neustarten",
|
||||
"uploadFolder": "Ordner hochladen",
|
||||
"openTerminal": "Terminal öffnen",
|
||||
"openLogs": "Logfiles öffnen"
|
||||
},
|
||||
"extractionInProgress": "Entpacken läuft",
|
||||
"pasteInProgress": "Einfügen läuft",
|
||||
"deleteInProgress": "Löschen läuft"
|
||||
"deleteInProgress": "Löschen läuft",
|
||||
"chownDialog": {
|
||||
"title": "Eigentümer ändern",
|
||||
"newOwner": "Neuer Eigentümer",
|
||||
"change": "Eigentümer ändern",
|
||||
"recursiveCheckbox": "Eigentümer rekursiv ändern"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Dateien hochladen ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "Eine oder mehrere Dateien sind bereits vorhanden.",
|
||||
"errorFailed": "Das Hochladen einer oder mehrerer Dateien ist fehlgeschlagen. Bitte erneut versuchen.",
|
||||
"closeWarning": "Die Seite nicht aktualisieren, bevor der Upload abgeschlossen ist.",
|
||||
"retry": "Erneut versuchen",
|
||||
"overwrite": "Überschreiben"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Extrahieren von {{ fileName }}",
|
||||
"closeWarning": "Die Seite nicht aktualisieren, bevor die Extraktion abgeschlossen ist."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "Die Datei hat ungespeicherte Änderungen",
|
||||
"details": "Änderungen gehen verloren, wenn sie nicht gespeichert werden",
|
||||
"dontSave": "Nicht speichern"
|
||||
},
|
||||
"notFound": "Nicht gefunden",
|
||||
"list": {
|
||||
"name": "Name",
|
||||
"size": "Größe",
|
||||
"owner": "Besitzer*in",
|
||||
"empty": "Keine Dateien",
|
||||
"symlink": "Symlink zu {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Umbenennen",
|
||||
"chown": "Besitzverhältnis ändern",
|
||||
"extract": "Hier auspacken",
|
||||
"download": "Herunterladen",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"cut": "Ausschneiden",
|
||||
"copy": "Kopieren",
|
||||
"paste": "Einfügen",
|
||||
"selectAll": "Alles Auswählen",
|
||||
"open": "Öffnen"
|
||||
},
|
||||
"mtime": "Geändert"
|
||||
},
|
||||
"extract": {
|
||||
"error": "Auspacken gescheitert: {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Bereits vorhanden"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Bereits vorhanden"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "Die Anwendung wird neugestartet"
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Hochladen",
|
||||
"exitWarning": "Aktuell werden noch Dateien hochgeladen. Wirklich schließen?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederherstellen",
|
||||
"save": "Speichern"
|
||||
}
|
||||
},
|
||||
"passwordReset": {
|
||||
"usernameOrEmail": "Username oder E-Mail-Adresse",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"done": "Done"
|
||||
},
|
||||
"username": "Username",
|
||||
"displayName": "Display name",
|
||||
"displayName": "Display Name",
|
||||
"actions": "Actions",
|
||||
"table": {
|
||||
"date": "Date",
|
||||
@@ -78,7 +78,7 @@
|
||||
"location": "Location",
|
||||
"locationPlaceholder": "Leave empty to use bare domain",
|
||||
"manualWarning": "Manually set up A (IPv4) and AAAA (IPv6) DNS records for <b>{{ location }}</b> pointing to this server",
|
||||
"userManagement": "User management",
|
||||
"userManagement": "User Management",
|
||||
"userManagementNone": "This app has its own user management. This setting determines whether this app is visible in the user's dashboard.",
|
||||
"userManagementMailbox": "All users with a mailbox on this Cloudron have access.",
|
||||
"userManagementLeaveToApp": "Leave user management to the app",
|
||||
@@ -124,7 +124,8 @@
|
||||
"settings": {
|
||||
"allowProfileEditCheckbox": "Allow users to edit their name and email",
|
||||
"require2FACheckbox": "Require users to set up 2FA",
|
||||
"saveAction": "Save"
|
||||
"saveAction": "Save",
|
||||
"title": "Settings"
|
||||
},
|
||||
"externalLdap": {
|
||||
"title": "Connect an External Directory",
|
||||
@@ -132,7 +133,7 @@
|
||||
"noopInfo": "LDAP authentication is not configured.",
|
||||
"provider": "Provider",
|
||||
"server": "Server URL",
|
||||
"acceptSelfSignedCert": "Accept Self-signed certificate",
|
||||
"acceptSelfSignedCert": "Accept Self-signed Certificate",
|
||||
"baseDn": "Base DN",
|
||||
"filter": "Filter",
|
||||
"usernameField": "Username Field",
|
||||
@@ -141,7 +142,7 @@
|
||||
"groupFilter": "Group Filter",
|
||||
"groupnameField": "Groupname Field",
|
||||
"auth": "Auth",
|
||||
"autocreateUsersOnLogin": "Automatically create users on login",
|
||||
"autocreateUsersOnLogin": "Auto-create Users on Login",
|
||||
"syncAction": "Sync",
|
||||
"configureAction": "Configure",
|
||||
"bindUsername": "Bind DN/Username (optional)",
|
||||
@@ -151,25 +152,25 @@
|
||||
},
|
||||
"addUserDialog": {
|
||||
"title": "Add User",
|
||||
"sendInviteCheckbox": "Send an invitation email now",
|
||||
"addUserAction": "Add User"
|
||||
"sendInviteCheckbox": "Send Invitation Email",
|
||||
"addUserAction": "Add"
|
||||
},
|
||||
"user": {
|
||||
"fullName": "Full name",
|
||||
"fullName": "Full Name",
|
||||
"username": "Username",
|
||||
"role": "Role",
|
||||
"groups": "Groups",
|
||||
"noGroups": "No groups available.",
|
||||
"usernamePlaceholder": "Optional. If not provided, user can pick during sign up",
|
||||
"displayName": "Display Name",
|
||||
"primaryEmail": "Primary email",
|
||||
"recoveryEmail": "Password recovery email",
|
||||
"activeCheckbox": "User is active",
|
||||
"primaryEmail": "Primary Email",
|
||||
"recoveryEmail": "Password Recovery Email",
|
||||
"activeCheckbox": "Active User",
|
||||
"displayNamePlaceholder": "Optional. If not provided, user can provide during sign up",
|
||||
"fallbackEmailPlaceholder": "If not specified, primary email will be used"
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
"title": "Delete user {{ username }}",
|
||||
"title": "Delete User {{ username }}",
|
||||
"description": "After deletion, the user will not be able to access the dashboard or login to any of the apps. Note that any user data inside the apps is not removed.",
|
||||
"deleteAction": "Delete"
|
||||
},
|
||||
@@ -183,14 +184,15 @@
|
||||
"group": {
|
||||
"name": "Name",
|
||||
"users": "Users",
|
||||
"addGroupAction": "Add Group"
|
||||
"addGroupAction": "Add",
|
||||
"allowedApps": "Allowed Apps"
|
||||
},
|
||||
"editGroupDialog": {
|
||||
"title": "Edit group {{ name }}",
|
||||
"title": "Edit Group {{ name }}",
|
||||
"externalLdapWarning": "This group is synced from the external LDAP directory."
|
||||
},
|
||||
"deleteGroupDialog": {
|
||||
"title": "Delete group {{ name }}",
|
||||
"title": "Delete Group {{ name }}",
|
||||
"description": "This group has {{ memberCount }} member(s). Really remove this group?",
|
||||
"deleteAction": "Delete"
|
||||
},
|
||||
@@ -220,10 +222,10 @@
|
||||
"descriptionEmail": "Send invite link"
|
||||
},
|
||||
"setGhostDialog": {
|
||||
"title": "Create password to impersonate {{ username }}",
|
||||
"description": "Set a temporary password to login on behalf of this user in apps or the dashboard. This password is valid for 6 hours.",
|
||||
"title": "Impersonate User {{ username }}",
|
||||
"description": "Set a temporary password to log in on behalf of this user to apps or the dashboard. This password is valid for 6 hours.",
|
||||
"password": "Temporary Password",
|
||||
"setPassword": "Set Password",
|
||||
"setPassword": "Set password",
|
||||
"generatePassword": "Generate Password"
|
||||
},
|
||||
"invitationNotification": {
|
||||
@@ -238,7 +240,7 @@
|
||||
},
|
||||
"secret": {
|
||||
"label": "Bind Password",
|
||||
"description": "All LDAP queries have to be authenticated with this secret and the user DN <i>{{ userDN }}</i>",
|
||||
"description": "Authenticate queries with the user DN <i>{{ userDN }}</i> and this secret",
|
||||
"url": "Server URL"
|
||||
},
|
||||
"cloudflarePortWarning": "Cloudflare proxying must be disabled on the dashboard domain to access the LDAP server",
|
||||
@@ -250,14 +252,14 @@
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile",
|
||||
"primaryEmail": "Primary email",
|
||||
"passwordRecoveryEmail": "Password recovery email",
|
||||
"primaryEmail": "Primary Email",
|
||||
"passwordRecoveryEmail": "Password Recovery Email",
|
||||
"language": "Language",
|
||||
"changePassword": {
|
||||
"title": "Change password",
|
||||
"currentPassword": "Current password",
|
||||
"newPassword": "New password",
|
||||
"newPasswordRepeat": "Repeat new password",
|
||||
"title": "Change Password",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"newPasswordRepeat": "Repeat New Password",
|
||||
"errorPasswordsDontMatch": "Passwords don't match"
|
||||
},
|
||||
"disable2FA": {
|
||||
@@ -276,14 +278,14 @@
|
||||
"title": "App Passwords",
|
||||
"app": "App",
|
||||
"name": "Name",
|
||||
"noPasswordsPlaceholder": "No App Passwords created",
|
||||
"noPasswordsPlaceholder": "No app passwords",
|
||||
"description": "App passwords are a security measure to protect your Cloudron user account. If you need to access a Cloudron app from an untrusted mobile app or client, you can log in with your username and the alternate password generated here."
|
||||
},
|
||||
"apiTokens": {
|
||||
"title": "API Tokens",
|
||||
"name": "Name",
|
||||
"description": "Use these personal access tokens to authenticate to the <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>",
|
||||
"noTokensPlaceholder": "No API Tokens created",
|
||||
"description": "Use these personal access tokens to authenticate to the <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>.",
|
||||
"noTokensPlaceholder": "No API tokens",
|
||||
"lastUsed": "Last Used",
|
||||
"neverUsed": "never",
|
||||
"scope": "Scope",
|
||||
@@ -295,12 +297,12 @@
|
||||
"loginTokens": {
|
||||
"title": "Login Tokens",
|
||||
"description": "You have {{ webadminTokenCount}} active web token(s) and {{ cliTokenCount }} CLI token(s).",
|
||||
"logoutAll": "Logout From All"
|
||||
"logoutAll": "Logout from all"
|
||||
},
|
||||
"changeEmail": {
|
||||
"title": "Change primary email address",
|
||||
"title": "Change Primary Email Address",
|
||||
"email": "New Email Address",
|
||||
"password": "Password for confirmation"
|
||||
"password": "Confirm with Password"
|
||||
},
|
||||
"changeFallbackEmail": {
|
||||
"title": "Change password recovery email address"
|
||||
@@ -322,7 +324,7 @@
|
||||
"access": "API Access",
|
||||
"allowedIpRanges": "Allowed IP Range(s)"
|
||||
},
|
||||
"changePasswordAction": "Change Password",
|
||||
"changePasswordAction": "Change password",
|
||||
"disable2FAAction": "Disable 2FA",
|
||||
"enable2FAAction": "Enable 2FA",
|
||||
"passwordResetNotification": {
|
||||
@@ -341,7 +343,7 @@
|
||||
"remount": "Remount Storage"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Schedule & Retention",
|
||||
"title": "Schedule & retention",
|
||||
"schedule": "Schedule",
|
||||
"retentionPolicy": "Retention Policy"
|
||||
},
|
||||
@@ -352,8 +354,8 @@
|
||||
"version": "Version",
|
||||
"noApps": "No Apps",
|
||||
"appCount": "{{ appCount }} App(s)",
|
||||
"tooltipDownloadBackupConfig": "Download Config",
|
||||
"cleanupBackups": "Cleanup Backups",
|
||||
"tooltipDownloadBackupConfig": "Download config",
|
||||
"cleanupBackups": "Cleanup backups",
|
||||
"backupNow": "Backup now",
|
||||
"tooltipPreservedBackup": "This backup will be preserved"
|
||||
},
|
||||
@@ -362,7 +364,9 @@
|
||||
"id": "Id",
|
||||
"date": "Date",
|
||||
"version": "Version",
|
||||
"list": "References backups of {{ appCount }} apps"
|
||||
"list": "References backups of {{ appCount }} app(s)",
|
||||
"size": "Size",
|
||||
"duration": "Duration"
|
||||
},
|
||||
"configureBackupSchedule": {
|
||||
"title": "Configure Backup Schedule and Retention",
|
||||
@@ -380,7 +384,7 @@
|
||||
"localDirectory": "Local backup directory",
|
||||
"hardlinksLabel": "Use hardlinks",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"acceptSelfSignedCerts": "Accept Self-signed certificate",
|
||||
"acceptSelfSignedCerts": "Accept Self-signed Certificate",
|
||||
"bucketName": "Bucket name",
|
||||
"prefix": "Prefix",
|
||||
"region": "Region",
|
||||
@@ -482,7 +486,7 @@
|
||||
"footer": {
|
||||
"title": "Footer"
|
||||
},
|
||||
"backgroundImage": "Login page background image"
|
||||
"backgroundImage": "Login Page Background"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Email",
|
||||
@@ -490,14 +494,15 @@
|
||||
"title": "Domains",
|
||||
"outbound": "Outbound only",
|
||||
"disabled": "Disabled",
|
||||
"stats": "Count: {{ mailboxCount }} / Usage: {{ usage }}",
|
||||
"testEmailTooltip": "Send Test Email"
|
||||
"stats": "Mailboxes: {{ mailboxCount }} / Usage: {{ usage }}",
|
||||
"testEmailTooltip": "Send test email",
|
||||
"inbound": "Inbound & Outbound"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"location": "Mail Server Location",
|
||||
"maxMailSize": "Maximum email size",
|
||||
"spamFilter": "Spam filtering",
|
||||
"maxMailSize": "Maximum Email Size",
|
||||
"spamFilter": "Spam Filtering",
|
||||
"spamFilterOverview": "{{ blacklistCount }} address(es) on the blocklist.",
|
||||
"solrFts": "Full Text Search",
|
||||
"acl": "Mail ACL",
|
||||
@@ -539,7 +544,7 @@
|
||||
},
|
||||
"spamFilterDialog": {
|
||||
"title": "Spam Filtering",
|
||||
"blacklisteAddresses": "Addresses on the blocklist",
|
||||
"blacklisteAddresses": "Email Address Blocklist",
|
||||
"blacklisteAddressesInfo": "Matched addresses will end up in the user's Spam folder. '*' and '?' glob patterns are supported.",
|
||||
"customRules": "Custom Spamassassin Rules",
|
||||
"blacklisteAddressesPlaceholder": "Line separated email address patterns",
|
||||
@@ -610,7 +615,7 @@
|
||||
"title": "Configure IPv6 Provider"
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "HTTP headers from matching IP addresses will be trusted",
|
||||
"description": "HTTP headers from matching IP addresses will be trusted.",
|
||||
"title": "Configure Trusted IPs",
|
||||
"summary": "{{ trustCount }} IPs trusted"
|
||||
},
|
||||
@@ -635,7 +640,7 @@
|
||||
"appstoreAccount": {
|
||||
"title": "Cloudron.io Account",
|
||||
"description": "A Cloudron.io account is used to manage your subscription.",
|
||||
"setupAction": "Set up Account",
|
||||
"setupAction": "Set up account",
|
||||
"subscription": "Subscription",
|
||||
"cloudronId": "Cloudron ID",
|
||||
"subscriptionEndsAt": "Canceled and ends on",
|
||||
@@ -655,12 +660,12 @@
|
||||
},
|
||||
"updates": {
|
||||
"title": "Updates",
|
||||
"checkForUpdatesAction": "Check for Updates",
|
||||
"checkForUpdatesAction": "Check for updates",
|
||||
"updateAvailableAction": "Update Available",
|
||||
"stopUpdateAction": "Stop Update",
|
||||
"disabled": "Disabled",
|
||||
"schedule": "Schedule",
|
||||
"description": "Platform and app updates are applied on the schedule set here, according to the <a href=\"/#/system-locale\">System Time Zone</a>.",
|
||||
"description": "Platform and app updates are applied on the schedule set here, using the <a href=\"/#/system-settings\">System Time Zone</a>.",
|
||||
"onLatest": "latest"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
@@ -738,19 +743,19 @@
|
||||
"domain": "Domain",
|
||||
"provider": "Provider",
|
||||
"renewCerts": {
|
||||
"title": "Renew certificates",
|
||||
"title": "Renew Certificates",
|
||||
"description": "Let's Encrypt certificates are renewed automatically. Use this option to trigger a renewal immediately.",
|
||||
"renewAllAction": "Renew All Certs"
|
||||
"renewAllAction": "Renew all certs"
|
||||
},
|
||||
"changeDashboardDomain": {
|
||||
"title": "Dashboard Domain",
|
||||
"description": "This will move the dashboard to the <code>my</code>subdomain of the selected domain.",
|
||||
"changeAction": "Change Domain"
|
||||
"changeAction": "Change domain"
|
||||
},
|
||||
"domainDialog": {
|
||||
"addTitle": "Add Domain",
|
||||
"editTitle": "Configure {{ domain }}",
|
||||
"addDescription": "Adding a domain lets you install apps on subdomains of this domain. Email settings for the domain can be configured in the Email view.",
|
||||
"addDescription": "Adding a domain lets you install apps on subdomains of this domain. Email settings can be configured in the Email view.",
|
||||
"domain": "Domain",
|
||||
"provider": "DNS Provider",
|
||||
"route53AccessKeyId": "Access Key Id",
|
||||
@@ -770,9 +775,9 @@
|
||||
"namecheapUsername": "Namecheap Username",
|
||||
"namecheapApiKey": "API Key",
|
||||
"namecheapInfo": "The server’s IP address must be added to the allowlist for this API key",
|
||||
"manualInfo": "All DNS records have to be set up manually before each app installation.",
|
||||
"manualInfo": "All DNS records must be set up manually before installing an app",
|
||||
"wildcardInfo": "Manually set up A (IPv4) and AAAA (IPv6) DNS records for <b>*.{{ domain }}.</b> and <b>{{ domain }}.</b> pointing to this server",
|
||||
"letsEncryptInfo": "Let's Encrypt requires your server to be reachable on port 80",
|
||||
"letsEncryptInfo": "Let's Encrypt requires your server to be reachable on port 80.",
|
||||
"advancedAction": "Advanced settings…",
|
||||
"zoneName": "Zone Name (Optional)",
|
||||
"fallbackCert": "Fallback Certificate (optional)",
|
||||
@@ -789,7 +794,7 @@
|
||||
"wellKnownDescription": "The values will be used to respond to <code>https://{{ domain }}/.well-known/</code> URLs. Note that an app must be available on the bare domain <code>{{ domain }}</code> for this to work. See the <a href=\"{{docsLink}}\" target=\"_blank\">docs</a> for more information.",
|
||||
"jitsiHostname": "Jitsi Location",
|
||||
"hetznerToken": "Hetzner Token",
|
||||
"cloudflareDefaultProxyStatus": "Enable proxying for new DNS records",
|
||||
"cloudflareDefaultProxyStatus": "Enable Proxying for New DNS Records",
|
||||
"porkbunApikey": "API Key",
|
||||
"porkbunSecretapikey": "Secret API Key",
|
||||
"bunnyAccessKey": "Bunny Access Key",
|
||||
@@ -804,7 +809,7 @@
|
||||
"gandiTokenTypePAT": "Personal Access Token (PAT)",
|
||||
"inwxUsername": "Username",
|
||||
"inwxPassword": "Password",
|
||||
"customNameservers": "Domain uses custom (vanity) nameservers"
|
||||
"customNameservers": "Domain Uses Custom (Vanity) Nameservers"
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Really remove {{ domain }}?",
|
||||
@@ -818,13 +823,13 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Well-Known locations of {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Well-Known Locations",
|
||||
"tooltipWellKnown": "Well-Known locations",
|
||||
"emptyPlaceholder": "No Domains",
|
||||
"noMatchesPlaceholder": "No matching domain"
|
||||
},
|
||||
"notifications": {
|
||||
"dismissTooltip": "Dismiss",
|
||||
"markAllAsRead": "Mark All as Read",
|
||||
"markAllAsRead": "Mark all as read",
|
||||
"settings": {
|
||||
"title": "Notification Settings",
|
||||
"backupFailed": "Backup failed",
|
||||
@@ -833,7 +838,7 @@
|
||||
"appUp": "App is back online",
|
||||
"appDown": "App is down",
|
||||
"rebootRequired": "Server reboot required",
|
||||
"cloudronUpdateFailed": "Cloudron Update Failed",
|
||||
"cloudronUpdateFailed": "Cloudron update failed",
|
||||
"diskSpace": "Low disk space"
|
||||
},
|
||||
"settingsDialog": {
|
||||
@@ -843,8 +848,8 @@
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
"clear": "Clear View",
|
||||
"download": "Download Full Logs"
|
||||
"clear": "Clear view",
|
||||
"download": "Download full logs"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -861,30 +866,103 @@
|
||||
"reallyDelete": "Really delete?"
|
||||
},
|
||||
"newDirectoryDialog": {
|
||||
"title": "New Folder"
|
||||
"title": "New Folder",
|
||||
"create": "Create"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"title": "New File",
|
||||
"create": "Create"
|
||||
},
|
||||
"renameDialog": {
|
||||
"reallyOverwrite": "A file with that name already exists. Overwrite existing file?"
|
||||
"reallyOverwrite": "A file with that name already exists. Overwrite existing file?",
|
||||
"title": "Rename {{ fileName }}",
|
||||
"newName": "New Name",
|
||||
"rename": "Rename"
|
||||
},
|
||||
"toolbar": {
|
||||
"new": "New",
|
||||
"upload": "Upload",
|
||||
"newFile": "New File",
|
||||
"newFolder": "New Folder",
|
||||
"uploadFile": "Upload File",
|
||||
"restartApp": "Restart App"
|
||||
"newFile": "New file",
|
||||
"newFolder": "New folder",
|
||||
"uploadFile": "Upload file",
|
||||
"restartApp": "Restart App",
|
||||
"uploadFolder": "Upload folder",
|
||||
"openTerminal": "Open Terminal",
|
||||
"openLogs": "Open Logs"
|
||||
},
|
||||
"extractionInProgress": "Extraction in progress",
|
||||
"pasteInProgress": "Pasting in progress",
|
||||
"deleteInProgress": "Deletion in progress"
|
||||
"deleteInProgress": "Deletion in progress",
|
||||
"chownDialog": {
|
||||
"title": "Change ownership",
|
||||
"newOwner": "New Owner",
|
||||
"change": "Change Owner",
|
||||
"recursiveCheckbox": "Change ownership recursively"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Uploading files ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "One or more files already exist.",
|
||||
"errorFailed": "Failed to upload one or more files. Please try again.",
|
||||
"closeWarning": "Do not refresh the page until upload has finished.",
|
||||
"retry": "Retry",
|
||||
"overwrite": "Overwrite"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Extracting {{ fileName }}",
|
||||
"closeWarning": "Do not refresh the page until extract has finished."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "File has unsaved changes",
|
||||
"details": "Your changes will be lost if you don't save them",
|
||||
"dontSave": "Don't Save"
|
||||
},
|
||||
"notFound": "Not found",
|
||||
"list": {
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"owner": "Owner",
|
||||
"empty": "No files",
|
||||
"symlink": "symlink to {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Rename",
|
||||
"chown": "Change ownership",
|
||||
"extract": "Extract Here",
|
||||
"download": "Download",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"selectAll": "Select All",
|
||||
"open": "Open"
|
||||
},
|
||||
"mtime": "Modified"
|
||||
},
|
||||
"extract": {
|
||||
"error": "Failed to extract: {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Already exists"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Already exists"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "restarting app"
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Uploading",
|
||||
"exitWarning": "Upload still in progress. Really close this page?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"save": "Save"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"config": {
|
||||
"title": "Email configuration {{ domain }}",
|
||||
"title": "Email Configuration {{ domain }}",
|
||||
"clientConfiguration": "Configuring Email Clients",
|
||||
"sending": {
|
||||
"title": "Sending"
|
||||
@@ -907,7 +985,8 @@
|
||||
"aliases": "Aliases",
|
||||
"usage": "Usage",
|
||||
"emptyPlaceholder": "No Mailboxes",
|
||||
"noMatchesPlaceholder": "No matching mailboxes"
|
||||
"noMatchesPlaceholder": "No matching mailboxes",
|
||||
"stats": "Count: {{ mailboxCount }} / Usage: {{ usage }}"
|
||||
},
|
||||
"mailinglists": {
|
||||
"title": "Mailing Lists",
|
||||
@@ -915,7 +994,7 @@
|
||||
"members": "List Members",
|
||||
"everyoneTooltip": "Posting allowed by non-members",
|
||||
"membersOnlyTooltip": "Posting restricted to members only",
|
||||
"emptyPlaceholder": "No Mailing Lists",
|
||||
"emptyPlaceholder": "No mailing lists",
|
||||
"noMatchesPlaceholder": "No matching mailing lists"
|
||||
},
|
||||
"catchall": {
|
||||
@@ -938,7 +1017,7 @@
|
||||
"mailRelay": {
|
||||
"host": "SMTP Host",
|
||||
"port": "SMTP Port (STARTTLS)",
|
||||
"selfsignedCheckbox": "Accept Self-signed certificate",
|
||||
"selfsignedCheckbox": "Accept Self-signed Certificate",
|
||||
"apiTokenOrKey": "API Token/Key",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
@@ -950,10 +1029,10 @@
|
||||
"description": "Masquerading allows users and apps to send emails with an arbitrary username in the FROM address."
|
||||
},
|
||||
"signature": {
|
||||
"title": "Signature",
|
||||
"title": "Email Signature",
|
||||
"description": "The text here will be attached to all emails going out from this domain.",
|
||||
"plainTextFormat": "Text format",
|
||||
"htmlFormat": "HTML format"
|
||||
"plainTextFormat": "Text Format",
|
||||
"htmlFormat": "HTML Format"
|
||||
},
|
||||
"dnsStatus": {
|
||||
"description": "Status of DNS Records may show an error while DNS is propagating (~5 minutes). See the <a href=\"{{ emailDnsDocsLink }}\" target=\"_blank\">troubleshooting</a> docs for help.",
|
||||
@@ -992,40 +1071,40 @@
|
||||
"incomingDisabledWarning": "Incoming email for this domain is not enabled."
|
||||
},
|
||||
"editMailboxDialog": {
|
||||
"title": "Edit mailbox {{ name }}@{{ domain }}",
|
||||
"title": "Edit Mailbox {{ name }}@{{ domain }}",
|
||||
"owner": "Mailbox Owner",
|
||||
"aliases": "Aliases",
|
||||
"noAliases": "No aliases are configured.",
|
||||
"addAliasAction": "Add an alias",
|
||||
"addAnotherAliasAction": "Add another alias",
|
||||
"enableStorageQuota": "Enable Storage Quota"
|
||||
"enableStorageQuota": "Storage Quota"
|
||||
},
|
||||
"deleteMailboxDialog": {
|
||||
"title": "Delete mailbox {{ name }}@{{ domain }}",
|
||||
"description": "After deletion, emails to this mailbox will bounce. You can choose to not delete emails in this mailbox for archival purposes. Archived emails are located at <code>/home/yellowtent/boxdata/mail/vmail</code> on the server.",
|
||||
"purgeMailboxCheckbox": "Delete all mails and filters inside this mailbox",
|
||||
"purgeMailboxCheckbox": "Delete All Mail and Filters in This Mailbox",
|
||||
"deleteAction": "Delete"
|
||||
},
|
||||
"addMailinglistDialog": {
|
||||
"title": "Add Mailing list",
|
||||
"members": "List Members",
|
||||
"membersOnlyCheckbox": "Restrict posting to members only",
|
||||
"membersOnlyCheckbox": "Restrict Posting to List Members",
|
||||
"name": "Name"
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
"title": "Edit Mailing list {{ name }}@{{ domain }}"
|
||||
"title": "Edit Mailing List {{ name }}@{{ domain }}"
|
||||
},
|
||||
"deleteMailinglistDialog": {
|
||||
"title": "Delete mailing list {{ name }}@{{ domain }}",
|
||||
"title": "Delete Mailing List {{ name }}@{{ domain }}",
|
||||
"description": "Really delete mailinglist <b>{{ name }}@{{ domain }}</b>?",
|
||||
"deleteAction": "Delete"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Mailing list is active"
|
||||
"activeCheckbox": "Active Mailing List"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "Mailbox is active",
|
||||
"enablePop3": "Enable POP3 access"
|
||||
"activeCheckbox": "Active Mailbox",
|
||||
"enablePop3": "POP3 Access"
|
||||
},
|
||||
"howToConnectInfoModal": "Configuring Email Clients"
|
||||
},
|
||||
@@ -1074,7 +1153,7 @@
|
||||
"userManagement": {
|
||||
"description": "This app is configured to authenticate with the Cloudron User Directory. This setting controls who can log in and use the app.",
|
||||
"descriptionSftp": "This setting also controls SFTP access.",
|
||||
"dashboardVisibility": "Dashboard visibility",
|
||||
"dashboardVisibility": "Dashboard Visibility",
|
||||
"visibleForAllUsers": "Visible to all users on this Cloudron",
|
||||
"visibleForSelected": "Only visible to the following users and groups"
|
||||
},
|
||||
@@ -1108,11 +1187,11 @@
|
||||
"appdata": {
|
||||
"title": "Data Directory",
|
||||
"description": "If the server is running out of disk space, use this to move the app's data to a <a href=\"/#/volumes\">volume</a>. Any data here is part of the app's backup.",
|
||||
"moveAction": "Move Data",
|
||||
"moveAction": "Move data",
|
||||
"mountTypeWarning": "The destination file system must support file permissions and ownership for the move to work"
|
||||
},
|
||||
"mounts": {
|
||||
"title": "Volume mounts",
|
||||
"title": "Volume Mounts",
|
||||
"volume": "Volume",
|
||||
"noMounts": "No volumes are mounted.",
|
||||
"addMountAction": "Add a volume mount",
|
||||
@@ -1134,8 +1213,8 @@
|
||||
"live": "Live",
|
||||
"1h": "1 hour"
|
||||
},
|
||||
"diskIOTotal": "Total Read: {{ read }} Total Write: {{ write }}",
|
||||
"networkIOTotal": "Total Inbound: {{ inbound }} Total Outbound: {{ outbound }}"
|
||||
"diskIOTotal": "Total read: {{ read }} Total write: {{ write }}",
|
||||
"networkIOTotal": "Total inbound: {{ inbound }} Total outbound: {{ outbound }}"
|
||||
},
|
||||
"email": {
|
||||
"from": {
|
||||
@@ -1167,7 +1246,7 @@
|
||||
"txtPlaceholder": "Leave empty to allow all bots to index this app",
|
||||
"disableIndexingAction": "Disable indexing"
|
||||
},
|
||||
"hstsPreload": "Enable HSTS preload for this site and all subdomains"
|
||||
"hstsPreload": "Enable HSTS Preload (including subdomains)"
|
||||
},
|
||||
"updates": {
|
||||
"info": {
|
||||
@@ -1191,11 +1270,11 @@
|
||||
"title": "Backups",
|
||||
"description": "Backups are complete snapshots of the app. You can use app backups to restore or clone this app.",
|
||||
"time": "Created At",
|
||||
"downloadConfigTooltip": "Download Config",
|
||||
"downloadConfigTooltip": "Download config",
|
||||
"cloneTooltip": "Clone",
|
||||
"restoreTooltip": "Restore",
|
||||
"createBackupAction": "Create Backup",
|
||||
"importAction": "Import Backup",
|
||||
"createBackupAction": "Create backup",
|
||||
"importAction": "Import backup",
|
||||
"downloadBackupTooltip": "Download",
|
||||
"checkIntegrity": "Check Integrity"
|
||||
},
|
||||
@@ -1214,12 +1293,12 @@
|
||||
"description": "To fix broken plugins or misconfiguration, place the app in Recovery Mode.",
|
||||
"restartAction": "Restart",
|
||||
"disableAction": "Disable Recovery Mode",
|
||||
"enableAction": "Enable Recovery Mode"
|
||||
"enableAction": "Enable recovery mode"
|
||||
},
|
||||
"taskError": {
|
||||
"title": "Task Error",
|
||||
"description": "If an installation, configuration, update, restore or backup action resulted in an error, you can retry the task.",
|
||||
"retryAction": "Retry {{ task }} Task"
|
||||
"retryAction": "Retry {{ task }} task"
|
||||
},
|
||||
"restart": {
|
||||
"title": "Restart",
|
||||
@@ -1440,7 +1519,7 @@
|
||||
"editVolumeDialog": {
|
||||
"title": "Edit volume {{ name }}"
|
||||
},
|
||||
"emptyPlaceholder": "No Volumes"
|
||||
"emptyPlaceholder": "No volumes"
|
||||
},
|
||||
"newLoginEmail": {
|
||||
"subject": "[<%= cloudron %>] New login on your account",
|
||||
@@ -1464,7 +1543,7 @@
|
||||
"id": "Client ID",
|
||||
"secret": "Client Secret",
|
||||
"signingAlgorithm": "Signing Algorithm",
|
||||
"loginRedirectUri": "Login callback URLs (comma separated)"
|
||||
"loginRedirectUri": "Login Callback URLs (comma separated)"
|
||||
},
|
||||
"description": "The OpenID provider can be used by external applications for single sign-on.",
|
||||
"editClientDialog": {
|
||||
@@ -1479,7 +1558,7 @@
|
||||
},
|
||||
"clients": {
|
||||
"title": "OpenID Clients",
|
||||
"empty": "No OpenID Clients"
|
||||
"empty": "No OpenID clients"
|
||||
}
|
||||
},
|
||||
"userdirectory": {
|
||||
@@ -1489,13 +1568,14 @@
|
||||
},
|
||||
"archives": {
|
||||
"listing": {
|
||||
"placeholder": "No Archived Apps"
|
||||
"placeholder": "No archived apps"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"target": {
|
||||
"label": "Backup Site",
|
||||
"size": "Size"
|
||||
"size": "Size",
|
||||
"fileCount": "Files"
|
||||
},
|
||||
"sites": {
|
||||
"title": "Backup Sites",
|
||||
@@ -1523,7 +1603,7 @@
|
||||
"dialog": {
|
||||
"title": "Docker Registry"
|
||||
},
|
||||
"emptyPlaceholder": "No Docker Registries"
|
||||
"emptyPlaceholder": "No docker registries"
|
||||
},
|
||||
"dockerRegistres": {
|
||||
"removeDialog": {
|
||||
@@ -1538,7 +1618,7 @@
|
||||
},
|
||||
"externallinks": {
|
||||
"label": "External Links",
|
||||
"description": "Add shortcuts to external services on the dashboard"
|
||||
"description": "Add shortcuts to external services on the dashboard."
|
||||
},
|
||||
"server": {
|
||||
"title": "Server"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1149,25 +1149,98 @@
|
||||
"newFolder": "Nouveau dossier",
|
||||
"newFile": "Nouveau fichier",
|
||||
"upload": "Charger",
|
||||
"new": "Nouveau"
|
||||
"new": "Nouveau",
|
||||
"uploadFolder": "Charger un dossier",
|
||||
"openTerminal": "Ouvrir le terminal",
|
||||
"openLogs": "Afficher les journaux"
|
||||
},
|
||||
"renameDialog": {
|
||||
"reallyOverwrite": "Un fichier portant ce nom existe déjà. Écraser le fichier existant ?"
|
||||
"reallyOverwrite": "Un fichier portant ce nom existe déjà. Écraser le fichier existant ?",
|
||||
"title": "Renommer {{ fileName }}",
|
||||
"newName": "Nouveau nom",
|
||||
"rename": "Renommer"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"create": "Créer",
|
||||
"title": "Nouveau fichier"
|
||||
},
|
||||
"newDirectoryDialog": {
|
||||
"title": "Nouveau dossier"
|
||||
"title": "Nouveau dossier",
|
||||
"create": "Créer"
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Voulez-vous vraiment supprimer ces fichiers ?"
|
||||
"reallyDelete": "Voulez-vous vraiment supprimer ces fichiers ?"
|
||||
},
|
||||
"title": "Gestionnaire de fichiers",
|
||||
"deleteInProgress": "Suppression en cours",
|
||||
"extractionInProgress": "Décompression en cours",
|
||||
"pasteInProgress": "Collage en cours"
|
||||
"pasteInProgress": "Collage en cours",
|
||||
"chownDialog": {
|
||||
"title": "Modifier la propriété",
|
||||
"newOwner": "Nouveau propriétaire",
|
||||
"change": "Modifier le propriétaire",
|
||||
"recursiveCheckbox": "Modifier récursivement la propriété"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Téléchargement des fichiers ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "Un ou plusieurs fichiers existent déjà.",
|
||||
"errorFailed": "Impossible de charger un ou plusieurs fichiers. Essayez à nouveau.",
|
||||
"closeWarning": "Ne rafraîchissez pas la page avant la fin du chargement.",
|
||||
"retry": "Réessayer",
|
||||
"overwrite": "Écraser"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Extraction en cours... {{ fileName }}",
|
||||
"closeWarning": "Ne rafraîchissez pas la page avant la fin de l'extraction."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "Le fichier comporte des modifications non sauvegardées",
|
||||
"details": "Vos modifications seront perdues si vous ne les sauvegardez pas",
|
||||
"dontSave": "Ne pas sauvegarder"
|
||||
},
|
||||
"notFound": "Non trouvé",
|
||||
"list": {
|
||||
"name": "Nom",
|
||||
"size": "Taille",
|
||||
"owner": "Propriétaire",
|
||||
"empty": "Aucun fichier",
|
||||
"symlink": "Symlink vers {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Renommer",
|
||||
"chown": "Modifier la propriété",
|
||||
"extract": "Extraire ici",
|
||||
"download": "Télécharger",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Modifier",
|
||||
"cut": "Couper",
|
||||
"copy": "Copier",
|
||||
"paste": "Coller",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"open": "Ouvrir"
|
||||
},
|
||||
"mtime": "Modifié"
|
||||
},
|
||||
"extract": {
|
||||
"error": "L'extraction a échoué : {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Le dossier existe déjà"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Le fichier existe déjà"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "Redémarrage de l'application..."
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Téléversement",
|
||||
"exitWarning": "Téléversement toujours en cours. Voulez-vous vraiment fermer cette page ?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Annuler",
|
||||
"redo": "Refaire",
|
||||
"save": "Enregistrer"
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"downloadAction": "Télécharger",
|
||||
|
||||
@@ -436,19 +436,84 @@
|
||||
"newFolder": "Nuova cartella",
|
||||
"newFile": "Nuovo documento",
|
||||
"upload": "Carica",
|
||||
"new": "Nuovo"
|
||||
"new": "Nuovo",
|
||||
"uploadFolder": "Carica cartella",
|
||||
"openTerminal": "Apri il terminale",
|
||||
"openLogs": "Vedi i logs"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"create": "Crea",
|
||||
"title": "Nuovo documento"
|
||||
},
|
||||
"newDirectoryDialog": {
|
||||
"title": "Nuova cartella"
|
||||
"title": "Nuova cartella",
|
||||
"create": "Crea"
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Eliminare davvero quanto segue?"
|
||||
},
|
||||
"title": "File Manager"
|
||||
"title": "File Manager",
|
||||
"renameDialog": {
|
||||
"title": "Rinomina {{ fileName }}",
|
||||
"newName": "Nuovo nome",
|
||||
"rename": "Rinomina"
|
||||
},
|
||||
"chownDialog": {
|
||||
"title": "Cambia proprietà",
|
||||
"newOwner": "Nuovo proprietario",
|
||||
"change": "Cambia proprietario",
|
||||
"recursiveCheckbox": "Cambia proprietario (ricorsivo)"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Carico documenti in corso ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "Uno o più documenti sono già esistenti.",
|
||||
"errorFailed": "Impossibile caricare uno o più file. Per favore riprova.",
|
||||
"closeWarning": "Non aggiornare la pagina fino al termine del caricamento.",
|
||||
"retry": "Riprova",
|
||||
"overwrite": "Sovrascrivi"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Estraggo {{ fileName }}",
|
||||
"closeWarning": "Non aggiornare la pagina fino al termine dell'estrazione."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "Il file ha dei cambiamenti non salvati",
|
||||
"details": "I cambiamenti verranno persi se non salvi documento prima di chiudere",
|
||||
"dontSave": "Non salvare"
|
||||
},
|
||||
"notFound": "Non trovato",
|
||||
"list": {
|
||||
"name": "Nome",
|
||||
"size": "Dimensione",
|
||||
"owner": "Proprietario",
|
||||
"empty": "Non ci sono documenti",
|
||||
"symlink": "symlink a {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Rinomina",
|
||||
"chown": "Cambia proprietario",
|
||||
"extract": "Estrai qui",
|
||||
"download": "Scarica",
|
||||
"delete": "Cancella",
|
||||
"edit": "Modifica",
|
||||
"cut": "Taglia",
|
||||
"copy": "Copia",
|
||||
"paste": "Incolla",
|
||||
"selectAll": "Seleziona Tutto"
|
||||
},
|
||||
"mtime": "Modificato"
|
||||
},
|
||||
"extract": {
|
||||
"error": "Errore nell'estrazione: {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Esiste già"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Già esistente"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "riavviando l'app"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"configureBackupStorage": {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"done": "Klaar"
|
||||
},
|
||||
"username": "Gebruikersnaam",
|
||||
"displayName": "Naam",
|
||||
"displayName": "Weergavenaam",
|
||||
"actions": "Acties",
|
||||
"table": {
|
||||
"date": "Datum",
|
||||
@@ -124,13 +124,14 @@
|
||||
"settings": {
|
||||
"require2FACheckbox": "Gebruikers moeten 2FA activeren",
|
||||
"saveAction": "Opslaan",
|
||||
"allowProfileEditCheckbox": "Sta gebruikers toe om hun naam en e-mail aan te passen"
|
||||
"allowProfileEditCheckbox": "Sta gebruikers toe om hun naam en e-mail aan te passen",
|
||||
"title": "Instellingen"
|
||||
},
|
||||
"externalLdap": {
|
||||
"title": "Verbind met een externe lijst",
|
||||
"noopInfo": "LDAP authenticatie is niet geconfigureerd.",
|
||||
"provider": "Aanbieder",
|
||||
"acceptSelfSignedCert": "Accepteer zelf-ondertekende certificaten",
|
||||
"acceptSelfSignedCert": "Accepteer zelf-ondertekend Certificaat",
|
||||
"baseDn": "Base DN",
|
||||
"filter": "Filter",
|
||||
"usernameField": "Veld voor gebruikersnaam",
|
||||
@@ -146,31 +147,31 @@
|
||||
"errorSelfSignedCert": "Server gebruikt een ongeldig of zelf-ondertekend certificaat.",
|
||||
"description": "Deze instelling synchroniseert en authenticeert gebruikers en groepen van een extern LDAP of Active Directory server. De synchronisatie is periodiek maar kan ook handmatig gestart worden.",
|
||||
"auth": "Authenticatie",
|
||||
"autocreateUsersOnLogin": "Maak automatisch gebruikers bij inloggen",
|
||||
"autocreateUsersOnLogin": "Automatisch gebruikers aanmaken bij het inloggen",
|
||||
"disableWarning": "De authentificatie-bron van alle bestaande gebruikers zal worden omgezet naar authentificatie via de lokale wachtwoord database."
|
||||
},
|
||||
"addUserDialog": {
|
||||
"addUserAction": "Gebruiker toevoegen",
|
||||
"addUserAction": "Toevoegen",
|
||||
"title": "Gebruiker toevoegen",
|
||||
"sendInviteCheckbox": "Stuur nu een uitnodigingsmail"
|
||||
"sendInviteCheckbox": "Uitnodigingsmail verzenden"
|
||||
},
|
||||
"user": {
|
||||
"fullName": "Volledige naam",
|
||||
"fullName": "Volledige Naam",
|
||||
"username": "Gebruikersnaam",
|
||||
"role": "Rol",
|
||||
"groups": "Groepen",
|
||||
"noGroups": "Geen groepen beschikbaar.",
|
||||
"displayName": "Weergavenaam",
|
||||
"primaryEmail": "Primair e-mailadres",
|
||||
"recoveryEmail": "E-mailadres voor wachtwoordherstel",
|
||||
"activeCheckbox": "Gebruiker is actief",
|
||||
"primaryEmail": "Primair E-mailadres",
|
||||
"recoveryEmail": "Wachtwoordherstel e-mailadres",
|
||||
"activeCheckbox": "Actieve Gebruiker",
|
||||
"usernamePlaceholder": "Optioneel. Indien niet ingevuld mag de gebruiker bij registratie zelf kiezen",
|
||||
"fallbackEmailPlaceholder": "Indien niet ingevoerd zal de primaire e-mail gebruikt worden",
|
||||
"displayNamePlaceholder": "Optioneel. Indien niet ingevoerd kan de gebruiker het kiezen tijdens eerste aanmelding"
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
"deleteAction": "Verwijder",
|
||||
"title": "Verwijder gebruiker {{ username }}",
|
||||
"title": "Verwijder Gebruiker {{ username }}",
|
||||
"description": "Na verwijdering heeft de gebruiker geen toegang meer tot het Dashboard of apps. Let op: de gebruikersgegevens in de apps worden niet verwijderd."
|
||||
},
|
||||
"editUserDialog": {
|
||||
@@ -183,7 +184,8 @@
|
||||
"group": {
|
||||
"name": "Naam",
|
||||
"users": "Gebruikers",
|
||||
"addGroupAction": "Groep toevoegen"
|
||||
"addGroupAction": "Toevoegen",
|
||||
"allowedApps": "Toegestane Apps"
|
||||
},
|
||||
"editGroupDialog": {
|
||||
"title": "Groep {{ name }} bewerken",
|
||||
@@ -220,8 +222,8 @@
|
||||
"descriptionEmail": "Stuur uitnodigingslink"
|
||||
},
|
||||
"setGhostDialog": {
|
||||
"description": "Stel een tijdelijk wachtwoord in namens deze gebruiker in apps of het Dashboard. Dit wachtwoord is 6 uur geldig.",
|
||||
"title": "Maak een wachtwoord om {{ username }} na te bootsen",
|
||||
"description": "Stel een tijdelijk wachtwoord in om namens deze gebruiker in te loggen bij apps of het dashboard. Dit wachtwoord is 6 uur geldig.",
|
||||
"title": "Impersoneren als gebruiker {{ username }}",
|
||||
"password": "Tijdelijk Wachtwoord",
|
||||
"setPassword": "Wachtwoord instellen",
|
||||
"generatePassword": "Genereer wachtwoord"
|
||||
@@ -238,7 +240,7 @@
|
||||
"description": "De LDAP server laat externe applicaties gebruikers authenticeren met de Cloudron gebruikerslijst.",
|
||||
"secret": {
|
||||
"label": "Koppel wachtwoord",
|
||||
"description": "Alle LDAP verzoeken moeten geauthentiseerd worden met dit geheim en de gebruiker DN <i>{{ userDN }}</i>",
|
||||
"description": "Authenticeer queries met de DN van de gebruiker <i>{{ userDN }}</i> en dit geheim",
|
||||
"url": "Server URL"
|
||||
},
|
||||
"cloudflarePortWarning": "Cloudflare proxy moet uitgeschakeld zijn op het domein van het dashboard om de LDAP server te kunnen bereiken",
|
||||
@@ -254,11 +256,11 @@
|
||||
"passwordRecoveryEmail": "Wachtwoordherstel e-mailadres",
|
||||
"language": "Taal",
|
||||
"changePassword": {
|
||||
"title": "Verander wachtwoord",
|
||||
"newPassword": "Nieuw wachtwoord",
|
||||
"newPasswordRepeat": "Herhaal nieuw wachtwoord",
|
||||
"title": "Verander Wachtwoord",
|
||||
"newPassword": "Nieuw Wachtwoord",
|
||||
"newPasswordRepeat": "Herhaal Nieuw Wachtwoord",
|
||||
"errorPasswordsDontMatch": "Wachtwoorden komen niet overeen",
|
||||
"currentPassword": "Huidig wachtwoord"
|
||||
"currentPassword": "Huidig Wachtwoord"
|
||||
},
|
||||
"disable2FA": {
|
||||
"password": "Wachtwoord",
|
||||
@@ -275,15 +277,15 @@
|
||||
"appPasswords": {
|
||||
"app": "App",
|
||||
"name": "Naam",
|
||||
"noPasswordsPlaceholder": "Er zijn geen App wachtwoorden aangemaakt",
|
||||
"noPasswordsPlaceholder": "Geen app-wachtwoorden",
|
||||
"title": "App wachtwoorden",
|
||||
"description": "App wachtwoorden zijn een veiligheidsmiddel om je Cloudronaccount te beschermen. Indien je toegang wilt tot een Cloudron-app met een niet-vertrouwde mobiele app of andere software, kun je inloggen met je gebruikersnaam en app wachtwoord die je hier kunt aanmaken."
|
||||
},
|
||||
"apiTokens": {
|
||||
"title": "API Tokens",
|
||||
"name": "Naam",
|
||||
"noTokensPlaceholder": "Er zijn geen API Tokens aangemaakt",
|
||||
"description": "Gebruik deze persoonlijke toegangstokens voor authenticatie met de <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>",
|
||||
"noTokensPlaceholder": "Geen API-tokens",
|
||||
"description": "Gebruik deze persoonlijke toegangstokens voor authenticatie met de <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>.",
|
||||
"neverUsed": "nooit",
|
||||
"lastUsed": "Laatst gebruikt",
|
||||
"scope": "Bereik",
|
||||
@@ -300,7 +302,7 @@
|
||||
"changeEmail": {
|
||||
"title": "Primair e-mailadres aanpassen",
|
||||
"email": "Nieuw e-mailadres",
|
||||
"password": "Wachtwoord ter bevestiging"
|
||||
"password": "Bevestig met wachtwoord"
|
||||
},
|
||||
"changeFallbackEmail": {
|
||||
"title": "E-mailadres voor wachtwoordherstel wijzigen"
|
||||
@@ -322,7 +324,7 @@
|
||||
"access": "API toegang",
|
||||
"allowedIpRanges": "Toegestane IP Range(s)"
|
||||
},
|
||||
"changePasswordAction": "Verander wachtwoord",
|
||||
"changePasswordAction": "Verander Wachtwoord",
|
||||
"disable2FAAction": "Twee-Factor (2FA) authenticatie uitschakelen",
|
||||
"enable2FAAction": "Twee-Factor (2FA) authenticatie inschakelen",
|
||||
"passwordResetNotification": {
|
||||
@@ -341,7 +343,7 @@
|
||||
"remount": "Her-koppel Storage"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Planning & Bewaartermijn",
|
||||
"title": "Planning & bewaartermijn",
|
||||
"schedule": "Planning",
|
||||
"retentionPolicy": "Bewaartermijn"
|
||||
},
|
||||
@@ -354,7 +356,7 @@
|
||||
"cleanupBackups": "Backups opschonen",
|
||||
"backupNow": "Backup maken",
|
||||
"appCount": "{{ appCount }} App(s)",
|
||||
"tooltipDownloadBackupConfig": "Download Configuratie",
|
||||
"tooltipDownloadBackupConfig": "Download configuratie",
|
||||
"tooltipPreservedBackup": "Deze backup blijft behouden"
|
||||
},
|
||||
"backupDetails": {
|
||||
@@ -362,7 +364,9 @@
|
||||
"id": "Id",
|
||||
"date": "Datum",
|
||||
"version": "Versie",
|
||||
"list": "Bevat backups van {{appCount}} apps"
|
||||
"list": "Verwijst naar backups van {{ appCount }} app(s)",
|
||||
"size": "Grootte",
|
||||
"duration": "Duur"
|
||||
},
|
||||
"configureBackupSchedule": {
|
||||
"title": "Configureer Backup Planning en Bewaartermijn",
|
||||
@@ -380,7 +384,7 @@
|
||||
"localDirectory": "Lokale backup map",
|
||||
"hardlinksLabel": "Gebruik hardlinks",
|
||||
"s3Endpoint": "Eindpunt",
|
||||
"acceptSelfSignedCerts": "Accepteer zelf-ondertekend certificaat",
|
||||
"acceptSelfSignedCerts": "Accepteer zelf-ondertekend Certificaat",
|
||||
"bucketName": "Bucket naam",
|
||||
"prefix": "Prefix",
|
||||
"region": "Regio",
|
||||
@@ -482,7 +486,7 @@
|
||||
"footer": {
|
||||
"title": "Voettekst"
|
||||
},
|
||||
"backgroundImage": "Inlogpagina achtergrond afbeelding"
|
||||
"backgroundImage": "Inlogpagina Achtergrond"
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-mail",
|
||||
@@ -491,12 +495,13 @@
|
||||
"testEmailTooltip": "Verstuur test e-mail",
|
||||
"outbound": "Alleen uitgaand",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"stats": "Aantal: {{ mailboxCount }} / Opslaggebruik: {{ usage }}"
|
||||
"stats": "Mailboxen: {{ mailboxCount }} / Opslaggebruik: {{ usage }}",
|
||||
"inbound": "Inkomend en Uitgaand"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"location": "Mail Server Locatie",
|
||||
"maxMailSize": "Maximale e-mail grootte",
|
||||
"maxMailSize": "Maximale e-mailgrootte",
|
||||
"spamFilter": "Spam filtering",
|
||||
"spamFilterOverview": "{{ blacklistCount }} adres(sen) op de blokkeerlijst.",
|
||||
"solrFts": "Zoek volledige tekst",
|
||||
@@ -539,7 +544,7 @@
|
||||
},
|
||||
"spamFilterDialog": {
|
||||
"title": "Spam Filtering",
|
||||
"blacklisteAddresses": "E-mailadressen op de blokkeer-lijst",
|
||||
"blacklisteAddresses": "Blokkeerlijst voor e-mailadressen",
|
||||
"customRules": "Aangepaste Spamassassin regels",
|
||||
"blacklisteAddressesPlaceholder": "Regel gescheiden e-mailadres patronen",
|
||||
"customRulesPlaceholder": "Aangepaste Spamassassin regels",
|
||||
@@ -589,7 +594,7 @@
|
||||
"nameComApiToken": "API Token",
|
||||
"namecheapUsername": "Namecheap Gebruikersnaam",
|
||||
"namecheapApiKey": "API Sleutel",
|
||||
"manualInfo": "Alle DNS records moeten handmatig ingesteld worden bij elke installatie voor elke app.",
|
||||
"manualInfo": "Alle DNS-records moeten handmatig worden aangemaakt voordat een app geïnstalleerd kan worden.",
|
||||
"wildcardInfo": "Stel handmatig A (IPv4) and AAAA (IPv6) DNS records in voor <b>*.{{ domain }}</b> en <b>{{ domain }}</b> met verwijzingen naar deze Cloudron server",
|
||||
"advancedAction": "Geavanceerde instellingen …",
|
||||
"zoneName": "Zone Naam (Optioneel)",
|
||||
@@ -598,8 +603,8 @@
|
||||
"fallbackCertCustomCertInfo": "Dit <a href=\"{{ customCertLink }}\" target=\"_blank\">wildcardcertificaat</a> wordt gebruikt voor alle apps van dit domein. Als dit niet het geval is, wordt een automatisch gegenereerd zelfondertekend certificaat gebruikt.",
|
||||
"fallbackCertKeyPlaceholder": "Sleutel",
|
||||
"fallbackCertCertificatePlaceholder": "Certificaat",
|
||||
"letsEncryptInfo": "Let's Encrypt vereist dat deze server bereikbaar is op poort 80",
|
||||
"addDescription": "Met het toevoegen van een domein krijg je de mogelijkheid om apps te installeren op subdomeinen. E-mailinstellingen voor het domein gaat via het E-mail scherm.",
|
||||
"letsEncryptInfo": "Let's Encrypt vereist dat deze server bereikbaar is op poort 80.",
|
||||
"addDescription": "Een domein toevoegen maakt het mogelijk om apps te installeren op subdomeinen van dit domein. E-mailinstellingen kunnen in het E-mailscherm worden geconfigureerd.",
|
||||
"domain": "Domein",
|
||||
"gcdnsServiceAccountKey": "Service Account Sleutel",
|
||||
"mastodonHostname": "Mastodon Server Locatie",
|
||||
@@ -611,7 +616,7 @@
|
||||
"jitsiHostname": "Jitsi Locatie",
|
||||
"wellKnownDescription": "De waardes worden gebruikt om te reageren op <code>https://{{ domain }}/.well-known/</code> URLs. Let op: de app moet bereikbaar zijn op het hoofddomein <code>{{ domain }}</code> om te kunnen werken. Lees de <a href=\"{{docsLink}}\" target=\"_blank\">documentatie</a> voor meer informatie.",
|
||||
"hetznerToken": "Hetzner Token",
|
||||
"cloudflareDefaultProxyStatus": "Inschakelen proxy voor nieuwe DNS regels",
|
||||
"cloudflareDefaultProxyStatus": "Inschakelen Proxy voor nieuwe DNS regels",
|
||||
"porkbunApikey": "API sleutel",
|
||||
"porkbunSecretapikey": "Geheime API sleutel",
|
||||
"bunnyAccessKey": "Bunny toegangssleutel",
|
||||
@@ -626,14 +631,14 @@
|
||||
"gandiTokenTypePAT": "Persoonlijke Toegang Token (PAT)",
|
||||
"inwxUsername": "Gebruikersnaam",
|
||||
"inwxPassword": "Wachtwoord",
|
||||
"customNameservers": "Domein maakt gebruik van aangepaste (eigen) nameservers"
|
||||
"customNameservers": "Domein maakt gebruik van aangepaste (eigen) naamservers"
|
||||
},
|
||||
"title": "Domeinen",
|
||||
"domain": "Domein",
|
||||
"provider": "Aanbieder",
|
||||
"renewCerts": {
|
||||
"title": "Vernieuw certificaten",
|
||||
"renewAllAction": "Vernieuw alle certificaten",
|
||||
"title": "Vernieuw Certificaten",
|
||||
"renewAllAction": "Vernieuw alle Certificaten",
|
||||
"description": "Let's Encrypt certificaten worden automatisch vernieuwd. Gebruik deze optie om nu te vernieuwen."
|
||||
},
|
||||
"changeDashboardDomain": {
|
||||
@@ -653,7 +658,7 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Well-Known locaties van {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Well-Known Locaties",
|
||||
"tooltipWellKnown": "Well-Known locaties",
|
||||
"emptyPlaceholder": "Geen Domeinen",
|
||||
"noMatchesPlaceholder": "Geen bijbehorende domein"
|
||||
},
|
||||
@@ -718,7 +723,7 @@
|
||||
"accessControl": {
|
||||
"userManagement": {
|
||||
"description": "Deze app is ingesteld voor authenticatie met het Cloudron Gebruikersadresboek. Deze instelling bepaalt wie kan inloggen om de app te gebruiken.",
|
||||
"dashboardVisibility": "Dashboard zichtbaarheid",
|
||||
"dashboardVisibility": "Dashboardzichtbaarheid",
|
||||
"visibleForSelected": "Alleen zichtbaar voor de volgende gebruikers en groepen",
|
||||
"descriptionSftp": "Deze instelling regelt ook SFTP-toegang.",
|
||||
"visibleForAllUsers": "Zichtbaar voor alle gebruikers op deze Cloudron"
|
||||
@@ -779,8 +784,8 @@
|
||||
"live": "Live",
|
||||
"1h": "1 uur"
|
||||
},
|
||||
"diskIOTotal": "Totaal Lezen: {{ read }}. .Totaal Schrijven: {{ write }}",
|
||||
"networkIOTotal": "Totaal Inkomend: {{ inbound }} Totaal Uitgaand: {{ outbound }}"
|
||||
"diskIOTotal": "Totaal gelezen: {{ read }} Totaal geschreven: {{ write }}",
|
||||
"networkIOTotal": "Totaal inkomend: {{ inbound }} Totaal uitgaand: {{ outbound }}"
|
||||
},
|
||||
"security": {
|
||||
"csp": {
|
||||
@@ -793,7 +798,7 @@
|
||||
"disableIndexingAction": "Indexering uitschakelen",
|
||||
"txtPlaceholder": "Leeg laten om toe te staan dat bots deze app indexeren"
|
||||
},
|
||||
"hstsPreload": "Schakel HSTS preload in voor deze site en alle subdomeinen"
|
||||
"hstsPreload": "Schakel HSTS-preload in (inclusief subdomeinen)"
|
||||
},
|
||||
"updates": {
|
||||
"info": {
|
||||
@@ -809,14 +814,14 @@
|
||||
"title": "Automatische Updates"
|
||||
},
|
||||
"updates": {
|
||||
"description": "Cloudron controleert periodiek de <a href=\"https://cloudron.io\" target=\"_blank\">App Store</a> voor updates."
|
||||
"description": "Cloudron controleert periodiek de App Store op updates."
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"backups": {
|
||||
"title": "Backups",
|
||||
"time": "Aangemaakt op",
|
||||
"downloadConfigTooltip": "Download configuratie",
|
||||
"downloadConfigTooltip": "Download Configuratie",
|
||||
"createBackupAction": "Maak backup",
|
||||
"importAction": "Importeer backup",
|
||||
"description": "Backups zijn complete momentopnamen van de app. Je kunt deze app backups gebruiken voor herstel of om de app te klonen.",
|
||||
@@ -838,13 +843,13 @@
|
||||
"recovery": {
|
||||
"title": "Herstel Modus",
|
||||
"restartAction": "Herstarten",
|
||||
"description": "Om defecte plugins of onjuiste configuraties te herstellen zet je de app in <a href=\"{{ docsLink }}\" target=\"_blank\">Herstel Modus</a>.",
|
||||
"description": "Om defecte plugins of onjuiste configuraties te herstellen zet je de app in Herstelmodus.",
|
||||
"disableAction": "Herstel Modus uitschakelen",
|
||||
"enableAction": "Herstel Modus inschakelen"
|
||||
"enableAction": "Herstelmodus inschakelen"
|
||||
},
|
||||
"taskError": {
|
||||
"title": "Taak fout",
|
||||
"retryAction": "Probeer Taak {{ task }} opnieuw",
|
||||
"retryAction": "Probeer taak {{ task }} opnieuw",
|
||||
"description": "Indien een installatie, configuratie, update, herstel of backup resulteert in een fout, probeer de taak dan opnieuw."
|
||||
},
|
||||
"restart": {
|
||||
@@ -1014,7 +1019,7 @@
|
||||
"title": "Configureer IPv6 aanbieder"
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "HTTP headers van bijbehorende IP adressen worden vertrouwd",
|
||||
"description": "HTTP headers van bijbehorende IP adressen worden vertrouwd.",
|
||||
"summary": "{{ trustCount }} IP’s vertrouwd",
|
||||
"title": "Configureer vertrouwde IP’s"
|
||||
},
|
||||
@@ -1061,7 +1066,7 @@
|
||||
"checkForUpdatesAction": "Controleer op updates",
|
||||
"updateAvailableAction": "Update beschikbaar",
|
||||
"stopUpdateAction": "Stop Update",
|
||||
"description": "Platform en app updates worden toegepast met deze planning, volgens deze <a href=\"/#/system-locale\">Systeem Tijdzone</a>.",
|
||||
"description": "Platform en app updates worden toegepast met deze planning en deze <a href=\"/#/system-locale\">Systeem Tijdzone</a>.",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"schedule": "Planning",
|
||||
"onLatest": "Laatste"
|
||||
@@ -1073,7 +1078,7 @@
|
||||
"days": "Dagen",
|
||||
"hours": "Uren",
|
||||
"title": "Automatische Update Planning configureren",
|
||||
"description": "Stel de dagen en uren in voor automatische platform- en app-updates. Zorg ervoor dat deze planning niet overlapt met de <a href=\"/#/backups\">backup planning</a>."
|
||||
"description": "Stel de dagen en uren in voor automatische updates van het platform en apps. Zorg ervoor dat dit schema niet overlapt met de back-upschema's."
|
||||
},
|
||||
"updateDialog": {
|
||||
"title": "Update Cloudron naar",
|
||||
@@ -1148,7 +1153,7 @@
|
||||
"appUp": "App is weer online",
|
||||
"appDown": "App werkt niet",
|
||||
"rebootRequired": "Server herstart noodzakelijk",
|
||||
"cloudronUpdateFailed": "Cloudron Update Mislukt",
|
||||
"cloudronUpdateFailed": "Cloudron update mislukt",
|
||||
"diskSpace": "Weinig diskruimte"
|
||||
},
|
||||
"settingsDialog": {
|
||||
@@ -1176,14 +1181,18 @@
|
||||
"reallyDelete": "Wil je het echt verwijderen?"
|
||||
},
|
||||
"newDirectoryDialog": {
|
||||
"title": "Nieuwe map"
|
||||
"title": "Nieuwe map",
|
||||
"create": "Aanmaken"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"title": "Nieuw bestand",
|
||||
"create": "Aanmaken"
|
||||
},
|
||||
"renameDialog": {
|
||||
"reallyOverwrite": "Een bestand met die naam bestaat al. Wil je het bestaande bestand overschrijven?"
|
||||
"reallyOverwrite": "Een bestand met die naam bestaat al. Wil je het bestaande bestand overschrijven?",
|
||||
"title": "Hernoem {{ fileName }}",
|
||||
"newName": "Nieuwe naam",
|
||||
"rename": "Hernoem"
|
||||
},
|
||||
"toolbar": {
|
||||
"new": "Nieuw",
|
||||
@@ -1191,15 +1200,84 @@
|
||||
"uploadFile": "Upload bestand",
|
||||
"restartApp": "Herstart app",
|
||||
"upload": "Upload",
|
||||
"newFolder": "Nieuwe map"
|
||||
"newFolder": "Nieuwe map",
|
||||
"uploadFolder": "Upload map",
|
||||
"openTerminal": "Open Terminal",
|
||||
"openLogs": "Open logbestanden"
|
||||
},
|
||||
"extractionInProgress": "Bezig met uitpakken",
|
||||
"pasteInProgress": "Bezig met plakken",
|
||||
"deleteInProgress": "Bezig met verwijderen"
|
||||
"deleteInProgress": "Bezig met verwijderen",
|
||||
"chownDialog": {
|
||||
"title": "Eigenaarschap veranderen",
|
||||
"newOwner": "Nieuwe eigenaar",
|
||||
"change": "Eigenaar aanpassen",
|
||||
"recursiveCheckbox": "Eigenaar recursief aanpassen"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Uploaden bestanden ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "Een of meerdere bestanden bestaan al.",
|
||||
"errorFailed": "Uploaden van een of meerdere bestanden is mislukt. Probeer opnieuw.",
|
||||
"closeWarning": "Herlaad deze pagina niet totdat het uploaden is afgerond.",
|
||||
"retry": "Probeer opnieuw",
|
||||
"overwrite": "Overschrijven"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Uitpakken {{ fileName }}",
|
||||
"closeWarning": "Herlaad deze pagina niet totdat het uitpakken is afgerond."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "Bestand heeft niet-opgeslagen veranderingen",
|
||||
"details": "Veranderingen gaan verloren als je ze nu niet opslaat",
|
||||
"dontSave": "Niet opslaan"
|
||||
},
|
||||
"notFound": "Niet gevonden",
|
||||
"list": {
|
||||
"name": "Naam",
|
||||
"size": "Grootte",
|
||||
"owner": "Eigenaar",
|
||||
"empty": "Geen bestanden",
|
||||
"symlink": "symlink naar {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Hernoem",
|
||||
"chown": "Eigenaarschap veranderen",
|
||||
"extract": "Hier uitpakken",
|
||||
"download": "Download",
|
||||
"delete": "Verwijderen",
|
||||
"edit": "Bewerk",
|
||||
"cut": "Knippen",
|
||||
"copy": "Kopiëren",
|
||||
"paste": "Plakken",
|
||||
"selectAll": "Alles selecteren",
|
||||
"open": "Open"
|
||||
},
|
||||
"mtime": "Bewerkt"
|
||||
},
|
||||
"extract": {
|
||||
"error": "Fout tijdens uitpakken: {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Bestaat al"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Bestaat al"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "herstarten app"
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Uploaden",
|
||||
"exitWarning": "Uploaden nog bezig. Weet je zeker dat je deze pagina wilt sluiten?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Ongedaan maken",
|
||||
"redo": "Opnieuw doen",
|
||||
"save": "Opslaan"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"config": {
|
||||
"title": "E-mailconfiguratie {{ domain }}",
|
||||
"title": "E-mailconfiguratie {{ domain }}",
|
||||
"clientConfiguration": "Configureren E-mail programma's",
|
||||
"sending": {
|
||||
"title": "Versturen"
|
||||
@@ -1221,7 +1299,8 @@
|
||||
"usage": "Gebruik",
|
||||
"title": "E-mailboxen",
|
||||
"emptyPlaceholder": "Geen Mailboxen",
|
||||
"noMatchesPlaceholder": "Geen bijbehorende mailboxen"
|
||||
"noMatchesPlaceholder": "Geen bijbehorende mailboxen",
|
||||
"stats": "Aantal: {{ mailboxCount }} / Opslaggebruik: {{ usage }}"
|
||||
},
|
||||
"mailinglists": {
|
||||
"title": "E-maillijsten",
|
||||
@@ -1229,7 +1308,7 @@
|
||||
"members": "Lijst van deelnemers",
|
||||
"everyoneTooltip": "Versturen naar deze lijst is toegestaan voor iedereen",
|
||||
"membersOnlyTooltip": "Versturen naar deze lijst is alleen toegestaan voor deelnemers",
|
||||
"emptyPlaceholder": "Geen Maillijsten",
|
||||
"emptyPlaceholder": "Geen maillijsten",
|
||||
"noMatchesPlaceholder": "Geen bijbehorende maillijsten"
|
||||
},
|
||||
"catchall": {
|
||||
@@ -1251,7 +1330,7 @@
|
||||
"mailRelay": {
|
||||
"host": "SMTP Host",
|
||||
"port": "SMTP Poort (STARTTLS)",
|
||||
"selfsignedCheckbox": "Accepteer zelf-ondertekende certificaten",
|
||||
"selfsignedCheckbox": "Accepteer zelf-ondertekend Certificaat",
|
||||
"apiTokenOrKey": "API Token/Sleutel",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord",
|
||||
@@ -1265,10 +1344,10 @@
|
||||
"description": "Middels 'vermommen' kunnen gebruikers en apps e-mail versturen met elke willekeurige gebruikersnaam in het VAN adres."
|
||||
},
|
||||
"signature": {
|
||||
"title": "Handtekening",
|
||||
"title": "E-mailhandtekening",
|
||||
"description": "Deze tekst wordt toegevoegd aan alle uitgaande e-mails van dit domein.",
|
||||
"plainTextFormat": "Tekstformaat",
|
||||
"htmlFormat": "HTML formaat"
|
||||
"htmlFormat": "HTML-formaat"
|
||||
},
|
||||
"dnsStatus": {
|
||||
"namecheapInfo": "Namecheap vereist handmatige handelingen voor MX records",
|
||||
@@ -1313,7 +1392,7 @@
|
||||
"noAliases": "Er zijn geen aliassen ingesteld.",
|
||||
"addAliasAction": "Alias toevoegen",
|
||||
"addAnotherAliasAction": "Een andere alias toevoegen",
|
||||
"enableStorageQuota": "Inschakelen Opslag Quota"
|
||||
"enableStorageQuota": "Opslagquota"
|
||||
},
|
||||
"deleteMailboxDialog": {
|
||||
"purgeMailboxCheckbox": "Verwijder alle e-mails en filters in deze mailbox",
|
||||
@@ -1328,19 +1407,19 @@
|
||||
"name": "Naam"
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
"title": "Bewerk Maillijst {{ name }}@{{ domain }}"
|
||||
"title": "Bewerk Mailinglijst {{ name }}@{{ domain }}"
|
||||
},
|
||||
"deleteMailinglistDialog": {
|
||||
"title": "Verwijder maillijst {{ name }}@{{ domain }}",
|
||||
"title": "Verwijder Mailinglijst {{ name }}@{{ domain }}",
|
||||
"deleteAction": "Verwijder",
|
||||
"description": "Weet je zeker dat je maillijst <b>{{ name }}@{{ domain }}</b> wilt verwijderen?"
|
||||
},
|
||||
"updateMailboxDialog": {
|
||||
"activeCheckbox": "Mailbox is actief",
|
||||
"enablePop3": "POP3 toegang inschakelen"
|
||||
"activeCheckbox": "Actieve Mailbox",
|
||||
"enablePop3": "POP3-toegang"
|
||||
},
|
||||
"updateMailinglistDialog": {
|
||||
"activeCheckbox": "Mailing-lijst is actief"
|
||||
"activeCheckbox": "Actieve Mailinglijst"
|
||||
},
|
||||
"howToConnectInfoModal": "Configureren e-mail programma's"
|
||||
},
|
||||
@@ -1351,7 +1430,8 @@
|
||||
"2faToken": "2FA Token",
|
||||
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"errorIncorrect2FAToken": "2FA token is niet geldig",
|
||||
"errorInternal": "Interne fout, probeer later opnieuw"
|
||||
"errorInternal": "Interne fout, probeer later opnieuw",
|
||||
"loginAction": "Inloggen"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Wachtwoord herstellen",
|
||||
@@ -1399,7 +1479,7 @@
|
||||
"editVolumeDialog": {
|
||||
"title": "Bewerk volume {{ name }}"
|
||||
},
|
||||
"emptyPlaceholder": "Geen Volumes"
|
||||
"emptyPlaceholder": "Geen volumes"
|
||||
},
|
||||
"passwordResetEmail": {
|
||||
"subject": "[<%= cloudron %>] Wachtwoord herstellen",
|
||||
@@ -1450,7 +1530,7 @@
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
"description": "Gekoppelde <a href=\"/#/volumes\">volumes</a> kunnen bereikt worden via <code>/media/(volume name)</code> . Gekoppelde data is niet opgenomen in de app's backup."
|
||||
"description": "Gekoppelde volumes kunnen bereikt worden via <code>/media/(volume name)</code> . Gekoppelde data is niet opgenomen in de backup van de app."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
@@ -1463,7 +1543,7 @@
|
||||
"id": "Client ID",
|
||||
"secret": "Client geheim",
|
||||
"signingAlgorithm": "Ondertekeningsalgoritme",
|
||||
"loginRedirectUri": "Login callback URLs (met komma gescheiden)"
|
||||
"loginRedirectUri": "Login Callback URL's (met komma gescheiden)"
|
||||
},
|
||||
"description": "De OpenID aanbieder kan gebruikt worden door externe applicaties voor single sign-on.",
|
||||
"editClientDialog": {
|
||||
@@ -1478,7 +1558,7 @@
|
||||
},
|
||||
"clients": {
|
||||
"title": "OpenID Clients",
|
||||
"empty": "Geen OpenID Clients"
|
||||
"empty": "Geen OpenID clients"
|
||||
}
|
||||
},
|
||||
"userdirectory": {
|
||||
@@ -1488,13 +1568,14 @@
|
||||
},
|
||||
"archives": {
|
||||
"listing": {
|
||||
"placeholder": "Geen Gearchiveerde Apps"
|
||||
"placeholder": "Geen gearchiveerde apps"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"target": {
|
||||
"label": "Backup Locatie",
|
||||
"size": "Grootte"
|
||||
"size": "Grootte",
|
||||
"fileCount": "Bestanden"
|
||||
},
|
||||
"sites": {
|
||||
"title": "Backup Locaties",
|
||||
@@ -1513,7 +1594,7 @@
|
||||
"provider": "Aanbieder",
|
||||
"username": "Gebruikersnaam",
|
||||
"title": "Docker Registries",
|
||||
"description": "Cloudron kan <a href=\"{{ customAppsLink }}\" target=\"_blank\">custom apps</a> binnenhalen en installeren van een private docker registry.",
|
||||
"description": "Cloudron kan aangepaste apps ophalen en installeren vanuit een privé Docker-registry.",
|
||||
"removeDialog": {
|
||||
"title": "Verwijder {{ serverAddress }}"
|
||||
},
|
||||
@@ -1522,7 +1603,7 @@
|
||||
"dialog": {
|
||||
"title": "Docker Registry"
|
||||
},
|
||||
"emptyPlaceholder": "Geen Docker Registries"
|
||||
"emptyPlaceholder": "Geen Docker registries"
|
||||
},
|
||||
"dockerRegistres": {
|
||||
"removeDialog": {
|
||||
@@ -1537,7 +1618,7 @@
|
||||
},
|
||||
"externallinks": {
|
||||
"label": "Externe Links",
|
||||
"description": "Voeg snelkoppeling toe op het dashboard naar externe diensten"
|
||||
"description": "Voeg snelkoppelingen naar externe diensten toe aan het dashboard."
|
||||
},
|
||||
"server": {
|
||||
"title": "Server"
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"nosso": "Iniciar sessão com conta dedicada",
|
||||
"email": "Iniciar sessão com endereço de correio eletrónico",
|
||||
"openid": "Iniciar a sessão com Couldron OpenID"
|
||||
}
|
||||
},
|
||||
"noMatchesPlaceholder": "Sem aplicações correspondentes"
|
||||
},
|
||||
"main": {
|
||||
"displayName": "Nome a exibir",
|
||||
@@ -39,7 +40,8 @@
|
||||
"username": "Nome de Utilizador",
|
||||
"actions": "Ações",
|
||||
"table": {
|
||||
"date": "Data"
|
||||
"date": "Data",
|
||||
"version": "Versão"
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Reiniciar",
|
||||
@@ -84,10 +86,11 @@
|
||||
"groups": "Grupos",
|
||||
"configuredForCloudronEmail": "Esta aplicação está pré-configurada para ser utilizada com o <a href=\"{{ emailDocsLink }}\" target=\"_blank\">E-mail do Cloudron</a>.",
|
||||
"cloudflarePortWarning": "O proxy do Cloudflare deve estar desativado para o domínio da aplicação para que possa aceder a esta porta",
|
||||
"portReadOnly": "apenas de leitura"
|
||||
"portReadOnly": "apenas de leitura",
|
||||
"ephemeralPortWarning": "Utilizar portas efémeras pode causar conflitos imprevisíveis."
|
||||
},
|
||||
"title": "Loja de Aplicações",
|
||||
"searchPlaceholder": "Procure por alternativas, tais como Github, Dropbox, Slack, Trello, …",
|
||||
"searchPlaceholder": "Procure por alternativas, tais como Github, Dropbox, Slack, Trello…",
|
||||
"unstable": "Instável",
|
||||
"appNotFoundDialog": {
|
||||
"description": "Não existe nenhuma aplicação <b>{{ appId }}</b> com a versão <b>{{ version }}</b>.",
|
||||
@@ -193,8 +196,11 @@
|
||||
"url": "URL do Servidor",
|
||||
"description": "Todas as consultas de LDAP tem de ser autenticadas com este segredo e o utilizador <i>{{ userDN }}</i> de DN"
|
||||
},
|
||||
"description": "O servidor LDAP pode ser utilizado pelas aplicações externas para autenticação.",
|
||||
"cloudflarePortWarning": "O proxy de Cloudflare deve estar desativado no domínio do painel para aceder ao servidor LDAP"
|
||||
"description": "O servidor LDAP permite que as aplicações externas autentiquem os utilizadores na diretoria de utilizadores do Cloudron.",
|
||||
"cloudflarePortWarning": "O proxy de Cloudflare deve estar desativado no domínio do painel para aceder ao servidor LDAP",
|
||||
"enable": "Ativar Servidor LDAP",
|
||||
"title": "Servidor LDAP",
|
||||
"enabled": "Ativar Servidor LDAP"
|
||||
},
|
||||
"users": {
|
||||
"superadminTooltip": "Este utilizador é um super administrador",
|
||||
@@ -208,13 +214,16 @@
|
||||
"usermanagerTooltip": "Este utilizador pode gerir os grupos e os outros utilizadores",
|
||||
"inactiveTooltip": "Utilizador está inativo",
|
||||
"externalLdapTooltip": "Da diretoria LDAP externa",
|
||||
"resetPasswordTooltip": "Redefinir Palavra-passe"
|
||||
"resetPasswordTooltip": "Redefinir Palavra-passe",
|
||||
"noMatchesPlaceholder": "Nenhum utilizador correspondente",
|
||||
"emptyPlaceholder": "Sem Utilizadores"
|
||||
},
|
||||
"groups": {
|
||||
"emptyPlaceholder": "Sem Grupos",
|
||||
"name": "Nome",
|
||||
"users": "Utilizadores",
|
||||
"externalLdapTooltip": "Da diretoria LDAP externa"
|
||||
"externalLdapTooltip": "Da diretoria LDAP externa",
|
||||
"noMatchesPlaceholder": "Nenhum grupo correspondente"
|
||||
},
|
||||
"user": {
|
||||
"fullName": "Nome Completo",
|
||||
@@ -324,7 +333,7 @@
|
||||
"invitationNotification": {
|
||||
"body": "Mensagem enviada para {{ email }}"
|
||||
},
|
||||
"title": "Utilizadores e Grupos"
|
||||
"title": "Utilizadores"
|
||||
},
|
||||
"login": {
|
||||
"2faToken": "Código 2FA",
|
||||
@@ -503,10 +512,10 @@
|
||||
"contents": "Conteúdos",
|
||||
"version": "Versão",
|
||||
"noApps": "Sem Aplicações",
|
||||
"appCount": "{{ appCount }} aplicações",
|
||||
"appCount": "Aplicações: {{ appCount }}",
|
||||
"backupNow": "Copiar Agora",
|
||||
"tooltipPreservedBackup": "Esta cópia de segurança será preservada",
|
||||
"title": "Listagem",
|
||||
"title": "Cópias de Segurança do Sistema",
|
||||
"noBackups": "Sem Cópias de Segurança",
|
||||
"tooltipDownloadBackupConfig": "Transferir Configuração",
|
||||
"cleanupBackups": "Limpeza das Cópias de Segurança"
|
||||
@@ -516,7 +525,7 @@
|
||||
"id": "Id.",
|
||||
"date": "Data",
|
||||
"version": "Versão",
|
||||
"list": "Referencia as cópias de segurança de {{ appCount }} aplicações"
|
||||
"list": "Referencia as cópias de segurança de {{ appCount }} aplicação(ões)"
|
||||
}
|
||||
},
|
||||
"passwordReset": {
|
||||
@@ -885,5 +894,12 @@
|
||||
"target": {
|
||||
"label": "Site da Cópia de Segurança"
|
||||
}
|
||||
},
|
||||
"filemanager": {
|
||||
"list": {
|
||||
"menu": {
|
||||
"download": "Transferir"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1172,14 +1172,18 @@
|
||||
"filemanager": {
|
||||
"title": "Файловый менеджер",
|
||||
"newDirectoryDialog": {
|
||||
"title": "Новая папка"
|
||||
"title": "Новая папка",
|
||||
"create": "Создать"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"title": "Новый файл",
|
||||
"create": "Создать"
|
||||
},
|
||||
"renameDialog": {
|
||||
"reallyOverwrite": "Файл с таким именем уже существует. Хотите перезаписать его?"
|
||||
"reallyOverwrite": "Файл с таким именем уже существует. Хотите перезаписать его?",
|
||||
"title": "Переименовать {{ fileName }}",
|
||||
"newName": "Новое имя",
|
||||
"rename": "Переименовать"
|
||||
},
|
||||
"toolbar": {
|
||||
"new": "Новый",
|
||||
@@ -1187,14 +1191,83 @@
|
||||
"newFile": "Новый файл",
|
||||
"newFolder": "Новая папка",
|
||||
"uploadFile": "Загрузить файл",
|
||||
"restartApp": "Перезагрузить приложение"
|
||||
"restartApp": "Перезагрузить приложение",
|
||||
"uploadFolder": "Загрузить папку",
|
||||
"openTerminal": "Открыть Терминал",
|
||||
"openLogs": "Открыть логи"
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Действительно удалить?"
|
||||
},
|
||||
"extractionInProgress": "Идёт извлечение",
|
||||
"pasteInProgress": "Выполняется копирование / перемещение",
|
||||
"deleteInProgress": "Выполняется удаление"
|
||||
"deleteInProgress": "Выполняется удаление",
|
||||
"chownDialog": {
|
||||
"title": "Смена владельца",
|
||||
"newOwner": "Новый владелец",
|
||||
"change": "Изменить владельца",
|
||||
"recursiveCheckbox": "Изменить владельца рекурсивно"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Загрузка файлов ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "Один или несколько файлов уже существуют.",
|
||||
"errorFailed": "Не удалось загрузить один или несколько файлов. Пожалуйста, попробуйте снова.",
|
||||
"closeWarning": "Не обновляйте страницу, пока загрузка не будет завершена.",
|
||||
"retry": "Повторить",
|
||||
"overwrite": "Перезаписать"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Распаковываем {{ fileName }}",
|
||||
"closeWarning": "Не обновляйте страницу, пока распаковка не будет завершена."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "Файл содержит несохраненные изменения",
|
||||
"details": "Ваши изменения будут утеряны, если Вы не сохраните их",
|
||||
"dontSave": "Не сохранять"
|
||||
},
|
||||
"notFound": "Не найдено",
|
||||
"list": {
|
||||
"name": "Имя",
|
||||
"size": "Размер",
|
||||
"owner": "Владелец",
|
||||
"empty": "Нет файлов",
|
||||
"symlink": "Символическая ссылка на {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Переименовать",
|
||||
"chown": "Изменить владельца",
|
||||
"extract": "Распаковать здесь",
|
||||
"download": "Скачать",
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"cut": "Вырезать",
|
||||
"copy": "Скопировать",
|
||||
"paste": "Вставить",
|
||||
"selectAll": "Выбрать все",
|
||||
"open": "Открыть"
|
||||
},
|
||||
"mtime": "Изменён"
|
||||
},
|
||||
"extract": {
|
||||
"error": "Не удалось распаковать: {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Уже существует"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Уже существует"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "перезапускаем приложение"
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Загружаем",
|
||||
"exitWarning": "Загрузка ещё не завершена. Вы уверены, что хотите закрыть страницу?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Отменить операцию",
|
||||
"redo": "Повторить операцию",
|
||||
"save": "Сохранить"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"outbound": {
|
||||
|
||||
@@ -849,24 +849,97 @@
|
||||
"newFolder": "Thư mục mới",
|
||||
"newFile": "Tập tin mới",
|
||||
"upload": "Tải lên",
|
||||
"new": "Thêm mới"
|
||||
"new": "Thêm mới",
|
||||
"uploadFolder": "Tải thư mục lên",
|
||||
"openTerminal": "Mở màn hình terminal",
|
||||
"openLogs": "Mở log"
|
||||
},
|
||||
"renameDialog": {
|
||||
"reallyOverwrite": "Trùng tên tập tin hiện có. Ghi đè lên tập tin cũ?"
|
||||
"reallyOverwrite": "Trùng tên tập tin hiện có. Ghi đè lên tập tin cũ?",
|
||||
"title": "Đổi tên {{ fileName }}",
|
||||
"newName": "Tên mới",
|
||||
"rename": "Đổi tên"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"create": "Tạo",
|
||||
"title": "Tập tin mới"
|
||||
},
|
||||
"newDirectoryDialog": {
|
||||
"title": "Thư mục mới"
|
||||
"title": "Thư mục mới",
|
||||
"create": "Tạo"
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Chắc chắn xoá?"
|
||||
},
|
||||
"extractionInProgress": "Đang giải nén",
|
||||
"pasteInProgress": "Đang dán",
|
||||
"deleteInProgress": "Đang xoá"
|
||||
"deleteInProgress": "Đang xoá",
|
||||
"chownDialog": {
|
||||
"title": "Đổi quyền sở hữu",
|
||||
"newOwner": "Chủ sở hữu mới",
|
||||
"change": "Đổi chủ sở hữu",
|
||||
"recursiveCheckbox": "Đổi quyền sở hữu theo vòng lặp đệ quy"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "Đang tải lên các tập tin ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "Một hay nhiều tập tin trùng đã tồn tại.",
|
||||
"errorFailed": "Không tải lên được một hay nhiều tập tin. Xin thử lại.",
|
||||
"closeWarning": "Xin đừng làm mới trang đến khi việc tải lên đã hoàn thành.",
|
||||
"retry": "Thử lại",
|
||||
"overwrite": "Ghi đè lên"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Đang giải nén {{ fileName }}",
|
||||
"closeWarning": "Xin đừng làm mới trang cho đến khi việc giải nén đã xong."
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "Tập tin có những thay đổi chưa được lưu",
|
||||
"details": "Những thay đổi của bạn sẽ bị mất nếu bạn không lưu lại",
|
||||
"dontSave": "Không cần lưu"
|
||||
},
|
||||
"notFound": "Không tìm thấy",
|
||||
"list": {
|
||||
"name": "Tên",
|
||||
"size": "Kích cỡ",
|
||||
"owner": "Chủ sở hữu",
|
||||
"empty": "Không có tập tin nào",
|
||||
"symlink": "Liên kết symlink đến {{ target }}",
|
||||
"menu": {
|
||||
"rename": "Đổi tên",
|
||||
"chown": "Đổi quyền sở hữu",
|
||||
"extract": "Giải nén tại đây",
|
||||
"download": "Tải xuống",
|
||||
"delete": "Xoá",
|
||||
"edit": "Chỉnh sửa",
|
||||
"cut": "Cắt",
|
||||
"copy": "Sao chép",
|
||||
"paste": "Dán",
|
||||
"selectAll": "Chọn tất cả",
|
||||
"open": "Mở"
|
||||
},
|
||||
"mtime": "Đã chỉnh sửa"
|
||||
},
|
||||
"extract": {
|
||||
"error": "Không thể giải nén: {{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "Đã tồn tại"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "Đã tồn tại"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "đang khởi động lại app"
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Đang tải lên",
|
||||
"exitWarning": "Vẫn đang tải lên. Bạn có chắc muốn đóng trang này?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Hoàn tác",
|
||||
"redo": "Xóa hoàn tác",
|
||||
"save": "Lưu"
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"downloadAction": "Tải xuống",
|
||||
|
||||
@@ -671,7 +671,8 @@
|
||||
"filemanager": {
|
||||
"title": "文件管理器",
|
||||
"newDirectoryDialog": {
|
||||
"title": "新文件夹"
|
||||
"title": "新文件夹",
|
||||
"create": "创建"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"title": "新文件",
|
||||
@@ -683,10 +684,74 @@
|
||||
"newFile": "新文件",
|
||||
"uploadFile": "上传文件",
|
||||
"restartApp": "重启应用",
|
||||
"newFolder": "新文件夹"
|
||||
"newFolder": "新文件夹",
|
||||
"uploadFolder": "上传文件夹",
|
||||
"openTerminal": "打开终端",
|
||||
"openLogs": "打开日志"
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "确定要删除下列文件?"
|
||||
},
|
||||
"renameDialog": {
|
||||
"title": "重命名 {{ fileName }}",
|
||||
"newName": "新文件名",
|
||||
"rename": "重命名"
|
||||
},
|
||||
"chownDialog": {
|
||||
"title": "修改文件的拥有者",
|
||||
"newOwner": "拥有者",
|
||||
"change": "修改拥有者",
|
||||
"recursiveCheckbox": "遍历文件夹修改拥有者"
|
||||
},
|
||||
"uploadingDialog": {
|
||||
"title": "正在上传文件 ({{ countDone }}/{{ count }})",
|
||||
"errorAlreadyExists": "一个或多个文件已存在。",
|
||||
"errorFailed": "一个或多个文件上传失败。请重试。",
|
||||
"closeWarning": "在上传完成前请不要刷新此页面。",
|
||||
"retry": "重试",
|
||||
"overwrite": "覆盖"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "正在解压 {{ fileName }}",
|
||||
"closeWarning": "在解压完成前请不要刷新本页面。"
|
||||
},
|
||||
"textEditorCloseDialog": {
|
||||
"title": "文件有未保存的修改",
|
||||
"details": "如果不保存文件,您的修改将丢失",
|
||||
"dontSave": "不要保存"
|
||||
},
|
||||
"notFound": "找不到文件",
|
||||
"list": {
|
||||
"name": "名称",
|
||||
"size": "大小",
|
||||
"owner": "拥有者",
|
||||
"empty": "没有文件",
|
||||
"symlink": "软链接到 {{ target }}",
|
||||
"menu": {
|
||||
"rename": "重命名",
|
||||
"chown": "修改拥有者",
|
||||
"extract": "解压到此处",
|
||||
"download": "下载",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"cut": "剪切",
|
||||
"copy": "复制",
|
||||
"paste": "粘贴",
|
||||
"selectAll": "全选"
|
||||
},
|
||||
"mtime": "修改时间"
|
||||
},
|
||||
"extract": {
|
||||
"error": "解压失败:{{ message }}"
|
||||
},
|
||||
"newDirectory": {
|
||||
"errorAlreadyExists": "该目录已经存在"
|
||||
},
|
||||
"newFile": {
|
||||
"errorAlreadyExists": "该文件已经存在"
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "正在重启应用"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
|
||||
@@ -105,6 +105,7 @@ const subscriptionRequiredDialog = useTemplateRef('subscriptionRequiredDialog');
|
||||
const ready = ref(false);
|
||||
const view = ref('');
|
||||
const profile = ref({});
|
||||
const dashboardDomain = ref('');
|
||||
const subscription = ref({
|
||||
plan: {},
|
||||
});
|
||||
@@ -227,6 +228,7 @@ async function refreshConfigAndFeatures() {
|
||||
|
||||
config.value = result;
|
||||
features.value = result.features;
|
||||
dashboardDomain.value = result.adminDomain;
|
||||
}
|
||||
|
||||
async function onOnline() {
|
||||
@@ -239,6 +241,7 @@ provide('features', features);
|
||||
provide('profile', profile);
|
||||
provide('refreshProfile', refreshProfile);
|
||||
provide('refreshFeatures', refreshConfigAndFeatures);
|
||||
provide('dashboardDomain', dashboardDomain);
|
||||
|
||||
onMounted(async () => {
|
||||
const [error, result] = await provisionModel.status();
|
||||
@@ -302,7 +305,7 @@ onMounted(async () => {
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.GROUPS }" v-show="profile.isAtLeastUserManager" :href="VIEWS.GROUPS" @click="onSidebarClose()"><i class="fa fa-users fa-fw"></i> {{ $t('main.navbar.groups') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.LDAP }" v-show="profile.isAtLeastAdmin" :href="VIEWS.LDAP" @click="onSidebarClose()"><i class="fa fa-fw fa-users-rays"></i> LDAP</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.OPENID }" v-show="profile.isAtLeastAdmin" :href="VIEWS.OPENID" @click="onSidebarClose()"><i class="fa fa-fw fa-brands fa-openid"></i> OpenID</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.USER_DIRECTORY_SETTINGS }" v-show="profile.isAtLeastAdmin" :href="VIEWS.USER_DIRECTORY_SETTINGS" @click="onSidebarClose()"><i class="fa fa-fw fa-cog"></i> {{ $t('userdirectory.settings.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.USER_DIRECTORY_SETTINGS }" v-show="profile.isAtLeastAdmin" :href="VIEWS.USER_DIRECTORY_SETTINGS" @click="onSidebarClose()"><i class="fa fa-fw fa-screwdriver-wrench"></i> {{ $t('userdirectory.settings.title') }}</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -313,7 +316,7 @@ onMounted(async () => {
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.MAILBOXES }" :href="VIEWS.MAILBOXES" @click="onSidebarClose()"><i class="fa fa-fw fa-inbox"></i> {{ $t('email.incoming.mailboxes.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.MAILINGLISTS }" :href="VIEWS.MAILINGLISTS" @click="onSidebarClose()"><i class="fa fa-fw-solid fa-envelopes-bulk"></i> {{ $t('email.incoming.mailinglists.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.EMAIL_EVENTLOG }" v-show="profile.isAtLeastAdmin" :href="VIEWS.EMAIL_EVENTLOG" @click="onSidebarClose()"><i class="fa fa-fw fa-list-alt"></i> {{ $t('emails.eventlog.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.EMAIL_SETTINGS }" v-show="profile.isAtLeastAdmin" :href="VIEWS.EMAIL_SETTINGS" @click="onSidebarClose()"><i class="fa fa-fw fa-cog"></i> {{ $t('emails.settings.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: view === VIEWS.EMAIL_SETTINGS }" v-show="profile.isAtLeastAdmin" :href="VIEWS.EMAIL_SETTINGS" @click="onSidebarClose()"><i class="fa fa-fw fa-screwdriver-wrench"></i> {{ $t('emails.settings.title') }}</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
|
||||
@@ -1,33 +1,18 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { FormGroup, Radiobutton, MultiSelect } from '@cloudron/pankow';
|
||||
import UsersModel from '../models/UsersModel.js';
|
||||
import GroupsModel from '../models/GroupsModel.js';
|
||||
import { ACL_OPTIONS } from '../constants.js';
|
||||
|
||||
const usersModel = UsersModel.create();
|
||||
const groupsModel = GroupsModel.create();
|
||||
|
||||
const props = defineProps([ 'manifest', 'error', 'hideOptionalSsoOption' ]);
|
||||
const props = defineProps([ 'users', 'groups', 'manifest', 'error', 'hideOptionalSsoOption' ]);
|
||||
|
||||
const accessRestrictionOption = defineModel('option');
|
||||
const accessRestriction = defineModel('acl');
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
|
||||
const optionalSso = !!props.manifest.optionalSso;
|
||||
const cloudronAuth = !!(props.manifest.addons['ldap'] || props.manifest.addons['oidc'] || props.manifest.addons['proxyAuth']) && !props.hideOptionalSsoOption;
|
||||
|
||||
onMounted(async () => {
|
||||
let [error, result] = await usersModel.list();
|
||||
if (error) return console.error(error);
|
||||
result.forEach(u => u.username = u.username || u.email);
|
||||
users.value = result;
|
||||
|
||||
[error, result] = await groupsModel.list();
|
||||
if (error) return console.error(error);
|
||||
groups.value = result;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { prettyDate, prettyBinarySize, isValidDomain } from '@cloudron/pankow/ut
|
||||
import AccessControl from './AccessControl.vue';
|
||||
import PortBindings from './PortBindings.vue';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import DashboardModel from '../models/DashboardModel.js';
|
||||
import DomainsModel from '../models/DomainsModel.js';
|
||||
import { PROXY_APP_ID, ACL_OPTIONS } from '../constants.js';
|
||||
|
||||
@@ -18,9 +17,9 @@ const STEP = Object.freeze({
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const domainsModel = DomainsModel.create();
|
||||
const dashboardModel = DashboardModel.create();
|
||||
|
||||
const subscriptionRequiredDialog = inject('subscriptionRequiredDialog');
|
||||
const dashboardDomain = inject('dashboardDomain');
|
||||
|
||||
// reactive
|
||||
const busy = ref(false);
|
||||
@@ -32,7 +31,6 @@ const dialog = useTemplateRef('dialogHandle');
|
||||
const locationInput = useTemplateRef('locationInput');
|
||||
const description = computed(() => marked.parse(manifest.value.description || ''));
|
||||
const domains = ref([]);
|
||||
const dashboardDomain = ref('');
|
||||
|
||||
const formValid = computed(() => {
|
||||
if (!domain.value) return false;
|
||||
@@ -167,10 +165,6 @@ function onClose() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [error, result] = await dashboardModel.config();
|
||||
if (error) return console.error(error);
|
||||
|
||||
dashboardDomain.value = result.adminDomain;
|
||||
});
|
||||
|
||||
const screenshotsContainer = useTemplateRef('screenshotsContainer');
|
||||
@@ -212,7 +206,7 @@ defineExpose({
|
||||
domains.value = domainList;
|
||||
|
||||
// preselect with dashboard domain
|
||||
domain.value = (domains.value.find(d => d.domain === dashboardDomain.value) || domains.value[0]).domain;
|
||||
domain.value = domains.value.find(d => d.domain === dashboardDomain.value).domain;
|
||||
|
||||
tcpPorts.value = a.manifest.tcpPorts;
|
||||
udpPorts.value = a.manifest.udpPorts;
|
||||
@@ -231,7 +225,7 @@ defineExpose({
|
||||
for (const p in secondaryDomains.value) {
|
||||
const port = secondaryDomains.value[p];
|
||||
port.value = port.defaultValue;
|
||||
port.domain = domains.value[0].domain;
|
||||
port.domain = dashboardDomain.value;
|
||||
}
|
||||
|
||||
currentScreenshotPos = 0;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// for restore from archive or clone !
|
||||
|
||||
import { ref, useTemplateRef, computed } from 'vue';
|
||||
import { ref, useTemplateRef, computed, inject } from 'vue';
|
||||
import { InputGroup, FormGroup, TextInput, SingleSelect, Dialog } from '@cloudron/pankow';
|
||||
import { prettyLongDate } from '@cloudron/pankow/utils';
|
||||
import PortBindings from '../components/PortBindings.vue';
|
||||
@@ -14,6 +14,7 @@ const appsModel = AppsModel.create();
|
||||
const archivesModel = ArchivesModel.create();
|
||||
const domainsModel = DomainsModel.create();
|
||||
|
||||
const dashboardDomain = inject('dashboardDomain');
|
||||
const appId = ref(null);
|
||||
const dialog = useTemplateRef('dialog');
|
||||
const restoreArchive = ref({});
|
||||
@@ -119,7 +120,7 @@ defineExpose({
|
||||
|
||||
const app = archive.appConfig || {
|
||||
subdomain: '',
|
||||
domain: domains.value[0].domain,
|
||||
domain: dashboardDomain.value,
|
||||
secondaryDomains: [],
|
||||
portBindings: {}
|
||||
}; // pre-8.2 backups do not have appConfig
|
||||
@@ -129,7 +130,7 @@ defineExpose({
|
||||
|
||||
restoreLocation.value = app.subdomain;
|
||||
const d = domains.value.find(function (d) { return app.domain === d.domain; });
|
||||
restoreDomain.value = d ? d.domain : domains.value[0].domain; // try to pre-select the app's domain
|
||||
restoreDomain.value = d ? d.domain : dashboardDomain.value; // try to pre-select the app's domain
|
||||
restoreSecondaryDomains.value = {};
|
||||
needsOverwrite.value = false;
|
||||
restoreArchive.value = archive;
|
||||
|
||||
@@ -113,7 +113,7 @@ onMounted(async () => {
|
||||
|
||||
<FormGroup>
|
||||
<label for="providerInput">{{ $t('backups.configureBackupStorage.provider') }} <sup><a href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<SingleSelect id="providerInput" v-model="provider" :options="storageProviders" option-key="value" option-label="name" />
|
||||
<SingleSelect id="providerInput" v-model="provider" :options="storageProviders" option-key="value" option-label="name" required />
|
||||
</FormGroup>
|
||||
|
||||
<!-- mountpoint -->
|
||||
|
||||
@@ -29,7 +29,7 @@ const formError = ref({});
|
||||
const busy = ref(false);
|
||||
const enableForUpdates = ref(false);
|
||||
const provider = ref('');
|
||||
const includeExclude = ref('everything'); // or exclude, include
|
||||
const includeExclude = ref(''); // or exclude, include
|
||||
const contentOptions = ref([]);
|
||||
const contentInclude = ref([]);
|
||||
const contentExclude = ref([]);
|
||||
@@ -227,6 +227,12 @@ function onCancel() {
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
const isValid = ref(false);
|
||||
|
||||
function checkValidity() {
|
||||
isValid.value = form.value.checkValidity();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
async open() {
|
||||
step.value = 'storage';
|
||||
@@ -247,7 +253,7 @@ defineExpose({
|
||||
encryptionPasswordHint.value = '';
|
||||
encryptedFilenames.value = false;
|
||||
limits.value = {};
|
||||
includeExclude.value = 'everything';
|
||||
includeExclude.value = '';
|
||||
contentInclude.value = [];
|
||||
contentExclude.value = [];
|
||||
|
||||
@@ -282,6 +288,8 @@ defineExpose({
|
||||
});
|
||||
|
||||
dialog.value.open();
|
||||
|
||||
// checkValidity();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -291,9 +299,9 @@ defineExpose({
|
||||
<Dialog ref="dialog" :title="$t('backups.site.addDialog.title')">
|
||||
<div>
|
||||
<div v-if="step === 'storage'">
|
||||
<form @submit.prevent="onSubmit()" autocomplete="off" ref="form">
|
||||
<form @submit.prevent="onSubmit()" autocomplete="off" ref="form" @change="checkValidity()">
|
||||
<fieldset :disabled="busy">
|
||||
<input style="display: none;" type="submit"/>
|
||||
<input style="display: none;" type="submit" :disabled="!isValid"/>
|
||||
|
||||
<FormGroup>
|
||||
<label for="nameInput">{{ $t('backups.configureBackupStorage.name') }}</label>
|
||||
@@ -306,10 +314,10 @@ defineExpose({
|
||||
<label>{{ $t('backups.configureBackupStorage.backupContents.title') }}</label>
|
||||
<div description>{{ $t('backups.configureBackupStorage.backupContents.description') }}</div>
|
||||
<div>
|
||||
<Radiobutton v-model="includeExclude" value="everything" :label="$t('backups.configureBackupStorage.backupContents.everything')"/>
|
||||
<Radiobutton v-model="includeExclude" value="exclude" :label="$t('backups.configureBackupStorage.backupContents.excludeSelected')"/>
|
||||
<Radiobutton v-model="includeExclude" name="contentRadioGroup" value="everything" :label="$t('backups.configureBackupStorage.backupContents.everything')" required/>
|
||||
<Radiobutton v-model="includeExclude" name="contentRadioGroup" value="exclude" :label="$t('backups.configureBackupStorage.backupContents.excludeSelected')" required/>
|
||||
<MultiSelect v-model="contentExclude" v-if="includeExclude === 'exclude'" :options="contentOptions" :search-threshold="10" option-key="id" style="margin: 6px 0 6px 25px;"/>
|
||||
<Radiobutton v-model="includeExclude" value="include" :label="$t('backups.configureBackupStorage.backupContents.includeOnlySelected')"/>
|
||||
<Radiobutton v-model="includeExclude" name="contentRadioGroup" value="include" :label="$t('backups.configureBackupStorage.backupContents.includeOnlySelected')" required/>
|
||||
<MultiSelect v-model="contentInclude" v-if="includeExclude === 'include'" :options="contentOptions" :search-threshold="10" option-key="id" style="margin: 6px 0 6px 25px;"/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
@@ -370,7 +378,7 @@ defineExpose({
|
||||
|
||||
<div style="display: flex; gap: 6px; align-items: end;">
|
||||
<Button secondary v-if="!busy" :disabled="busy" @click="onCancel()">{{ $t('main.dialog.cancel') }}</Button>
|
||||
<Button primary :disabled="busy" :loading="busy" @click="onSubmit()">{{ useEncryption ? $t('main.action.next') : $t('main.dialog.save') }}</Button>
|
||||
<Button primary :disabled="busy || !isValid" :loading="busy" @click="onSubmit()">{{ useEncryption ? $t('main.action.next') : $t('main.dialog.save') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -18,7 +18,6 @@ const providers = [
|
||||
{ name: 'Google Cloud', value: 'google-cloud' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Quay', value: 'quay' },
|
||||
{ name: 'Treescale', value: 'treescale' },
|
||||
{ name: t('settings.registryConfig.providerOther') || 'Other', value: 'other' },
|
||||
];
|
||||
|
||||
|
||||
@@ -133,6 +133,10 @@ function onGcdnsFileInputChange(event) {
|
||||
<SingleSelect v-model="provider" @select="onProviderChange" :disabled="disabled" :options="DomainsModel.providers" option-key="value" option-label="name" required />
|
||||
</FormGroup>
|
||||
|
||||
<div class="warning-label" v-show="provider === 'wildcard'" v-html="$t('domains.domainDialog.wildcardInfo', { domain: domain })"></div>
|
||||
<div class="warning-label" v-show="provider === 'manual'" v-html="$t('domains.domainDialog.manualInfo')"></div>
|
||||
<div class="warning-label" v-show="needsPort80(provider, tlsProvider)" v-html="$t('domains.domainDialog.letsEncryptInfo')"></div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<FormGroup v-if="provider === 'route53'">
|
||||
<label for="accessKeyIdInput">{{ $t('domains.domainDialog.route53AccessKeyId') }}</label>
|
||||
@@ -259,7 +263,7 @@ function onGcdnsFileInputChange(event) {
|
||||
</FormGroup>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<FormGroup v-if="provider === 'hetzner'">
|
||||
<FormGroup v-if="provider === 'hetzner' || provider === 'hetznercloud'">
|
||||
<label for="hetznerTokenInput">{{ $t('domains.domainDialog.hetznerToken') }}</label>
|
||||
<MaskedInput id="hetznerTokenInput" v-model="dnsConfig.token" required />
|
||||
</FormGroup>
|
||||
@@ -320,9 +324,5 @@ function onGcdnsFileInputChange(event) {
|
||||
<SingleSelect v-model="tlsProvider" :options="tlsProviders" option-key="value" option-label="name"/>
|
||||
</FormGroup>
|
||||
|
||||
<div class="warning-label" v-show="provider === 'wildcard'" v-html="$t('domains.domainDialog.wildcardInfo', { domain: domain })"></div>
|
||||
<div class="warning-label" v-show="provider === 'manual'" v-html="$t('domains.domainDialog.manualInfo')"></div>
|
||||
<div class="warning-label" v-show="needsPort80(provider, tlsProvider)" v-html="$t('domains.domainDialog.letsEncryptInfo')"></div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,7 +60,7 @@ function cancel() {
|
||||
<Button tool plain secondary @click="cancel" :disabled="saving">{{ $t('main.dialog.cancel') }}</Button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="markdown" v-html="marked.parse(value)"></div>
|
||||
<div v-if="markdown" v-html="marked.parseInline(value)"></div>
|
||||
<div v-else>{{ value }}</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
@@ -64,7 +64,7 @@ const uploadMenuModel = [{
|
||||
action: onUploadFile,
|
||||
}, {
|
||||
icon: 'fa-regular fa-folder-open',
|
||||
label: t('filemanager.toolbar.newFolder'),
|
||||
label: t('filemanager.toolbar.uploadFolder'),
|
||||
action: onUploadFolder,
|
||||
}];
|
||||
|
||||
@@ -443,7 +443,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
appLink.value = `https://${result.body.fqdn}`;
|
||||
title.value = `${result.body.label || result.body.fqdn} (${result.body.manifest.title})`;
|
||||
title.value = `${result.body.label || result.body.fqdn} ` + (result.body.manifest ? `(${result.body.manifest.title})` : '');
|
||||
} else if (type === 'volume') {
|
||||
let error, result;
|
||||
try {
|
||||
|
||||
@@ -108,7 +108,7 @@ defineExpose({
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="appsInput">Access to Apps</label>
|
||||
<label for="appsInput">{{ $t('users.group.allowedApps') }}</label>
|
||||
<MultiSelect v-model="apps" :options="allApps" option-key="id" :search-threshold="20"/>
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
|
||||
@@ -33,6 +33,9 @@ const notificationsAllBusy = ref(false);
|
||||
|
||||
function onOpenNotifications(popover, event, elem) {
|
||||
popover.open(event, elem);
|
||||
|
||||
// close after 2 seconds if there is nothing to show
|
||||
if (notifications.value.length === 0) setTimeout(popover.close, 2000);
|
||||
}
|
||||
|
||||
async function onMarkNotificationRead(notification) {
|
||||
@@ -55,6 +58,8 @@ async function onMarkAllNotificationRead() {
|
||||
await refresh();
|
||||
|
||||
notificationsAllBusy.value = false;
|
||||
|
||||
if (notifications.value.length === 0) setTimeout(helpPopover.value.close, 2000);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
|
||||
@@ -57,7 +57,7 @@ onMounted(async () => {
|
||||
if (error) return console.error(error);
|
||||
|
||||
ldapUrl.value = `ldaps://${result.adminFqdn}:636`;
|
||||
adminDomain.value = domains.find(d => d.domain === result.adminDomain) || domains[0];
|
||||
adminDomain.value = domains.find(d => d.domain === result.adminDomain);
|
||||
|
||||
[error, result] = await userDirectoryModel.getExposedLdapConfig();
|
||||
if (error) return console.error(error);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, useTemplateRef, computed } from 'vue';
|
||||
import { ref, useTemplateRef, computed, inject } from 'vue';
|
||||
import { Dialog, Button, TextInput, FormGroup, Checkbox, InputGroup, SingleSelect } from '@cloudron/pankow';
|
||||
import { prettyDecimalSize } from '@cloudron/pankow/utils';
|
||||
import MailboxesModel from '../models/MailboxesModel.js';
|
||||
@@ -10,6 +10,7 @@ const props = defineProps([ 'apps', 'users', 'groups', 'domains' ]);
|
||||
|
||||
const mailboxesModel = MailboxesModel.create();
|
||||
|
||||
const dashboardDomain = inject('dashboardDomain');
|
||||
const dialog = useTemplateRef('dialog');
|
||||
const busy = ref(false);
|
||||
const formError = ref('');
|
||||
@@ -34,7 +35,8 @@ const domainHasIncomingEnabled = computed(() => {
|
||||
function onAddAlias() {
|
||||
aliases.value.push({
|
||||
name: '',
|
||||
domain: '@' + props.domains[0].domain,
|
||||
domain: dashboardDomain.value,
|
||||
label: '@' + dashboardDomain.value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,7 +93,7 @@ defineExpose({
|
||||
mailbox.value = m;
|
||||
|
||||
name.value = m ? m.name : '';
|
||||
domain.value = m ? m.domain : props.domains[0].domain;
|
||||
domain.value = m ? m.domain : dashboardDomain.value;
|
||||
ownerId.value = m ? m.ownerId : '';
|
||||
aliases.value = m ? m.aliases : [];
|
||||
active.value = m ? m.active : true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { ref, useTemplateRef, inject } from 'vue';
|
||||
import { Dialog, TextInput, FormGroup, Checkbox, InputGroup, SingleSelect } from '@cloudron/pankow';
|
||||
import MailinglistsModel from '../models/MailinglistsModel.js';
|
||||
|
||||
@@ -19,6 +19,7 @@ const membersText = ref('');
|
||||
const membersOnly = ref(false);
|
||||
const active = ref(true);
|
||||
const domainList = ref([]);
|
||||
const dashboardDomain = inject('dashboardDomain');
|
||||
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
@@ -63,7 +64,7 @@ defineExpose({
|
||||
mailinglist.value = m;
|
||||
|
||||
name.value = m ? m.name : '';
|
||||
domain.value = m ? m.domain : props.domains[0].domain;
|
||||
domain.value = m ? m.domain : dashboardDomain.value;
|
||||
membersText.value = m ? m.members.join('\n') : '';
|
||||
membersOnly.value = m ? m.membersOnly : false;
|
||||
active.value = m ? m.active : true;
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { FormGroup, MultiSelect } from '@cloudron/pankow';
|
||||
import UsersModel from '../models/UsersModel.js';
|
||||
import GroupsModel from '../models/GroupsModel.js';
|
||||
|
||||
defineProps(['hasFtp']);
|
||||
|
||||
const usersModel = UsersModel.create();
|
||||
const groupsModel = GroupsModel.create();
|
||||
defineProps(['hasFtp', 'users', 'groups']);
|
||||
|
||||
const accessRestriction = defineModel('acl');
|
||||
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
let [error, result] = await usersModel.list();
|
||||
if (error) return console.error(error);
|
||||
result.forEach(u => u.username = u.username || u.email);
|
||||
users.value = result;
|
||||
|
||||
[error, result] = await groupsModel.list();
|
||||
if (error) return console.error(error);
|
||||
groups.value = result;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@ const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Button, ClipboardAction, Menu, FormGroup, TextInput, Checkbox, TableView, Dialog } from '@cloudron/pankow';
|
||||
import { prettyLongDate, prettyFileSize } from '@cloudron/pankow/utils';
|
||||
import { prettyDuration, prettyLongDate, prettyFileSize } from '@cloudron/pankow/utils';
|
||||
import { TASK_TYPES } from '../constants.js';
|
||||
import Section from '../components/Section.vue';
|
||||
import BackupsModel from '../models/BackupsModel.js';
|
||||
@@ -56,6 +56,25 @@ const columns = {
|
||||
actions: {}
|
||||
};
|
||||
|
||||
const backupContentTableColumns = {
|
||||
label: {
|
||||
label: t('backups.listing.contents'),
|
||||
sort: true,
|
||||
},
|
||||
fileCount: {
|
||||
label: t('backup.target.fileCount'),
|
||||
sort(a, b, A, B) {
|
||||
return A.stats?.upload?.fileCount - B.stats?.upload?.fileCount;
|
||||
},
|
||||
},
|
||||
size: {
|
||||
label: t('backup.target.size'),
|
||||
sort(a, b, A , B) {
|
||||
return A.stats?.upload?.size - B.stats?.upload?.size;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const actionMenuModel = ref([]);
|
||||
const actionMenuElement = useTemplateRef('actionMenuElement');
|
||||
function onActionMenu(backup, event) {
|
||||
@@ -183,10 +202,13 @@ async function onDownloadConfig(backup) {
|
||||
|
||||
// backups info dialog
|
||||
const infoDialog = useTemplateRef('infoDialog');
|
||||
const infoDialogBusy = ref(true);
|
||||
const infoBackup = ref({ contents: [] });
|
||||
async function onInfo(backup) {
|
||||
infoBackup.value = backup;
|
||||
infoBackup.value.contents = [];
|
||||
infoDialogBusy.value = true;
|
||||
|
||||
infoDialog.value.open();
|
||||
|
||||
// amend detailed app info
|
||||
@@ -221,6 +243,8 @@ async function onInfo(backup) {
|
||||
}
|
||||
infoBackup.value.contents.push(content);
|
||||
}
|
||||
|
||||
infoDialogBusy.value = false;
|
||||
}
|
||||
|
||||
// edit backups dialog
|
||||
@@ -307,16 +331,35 @@ defineExpose({ refresh });
|
||||
<div class="info-label">{{ $t('backups.backupDetails.version') }}</div>
|
||||
<div class="info-value">{{ infoBackup.packageVersion }}</div>
|
||||
</div>
|
||||
<div class="info-row" v-if="infoBackup.stats?.aggregatedUpload">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.size') }}</div>
|
||||
<div class="info-value">{{ prettyFileSize(infoBackup.stats.aggregatedUpload.size) }} | {{ infoBackup.stats.aggregatedUpload.fileCount }} file(s)</div>
|
||||
</div>
|
||||
<div class="info-row" v-if="infoBackup.stats?.aggregatedCopy">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.duration') }}</div>
|
||||
<div class="info-value">{{ prettyDuration(infoBackup.stats.aggregatedUpload.duration + infoBackup.stats.aggregatedCopy.duration) }}</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div>{{ $t('backups.backupDetails.list', { appCount: infoBackup.appCount }) }}:</div>
|
||||
<br/>
|
||||
|
||||
<p class="text-muted">{{ $t('backups.backupDetails.list', { appCount: infoBackup.contents.length }) }}:</p>
|
||||
<div v-for="content in infoBackup.contents" :key="content.id">
|
||||
<a v-if="content.id === 'mail'" href="/#/mailboxes">{{ content.label }}</a>
|
||||
<a v-else-if="content.fqdn" :href="`/#/app/${content.id}/backups`">{{ content.label || content.fqdn }}</a>
|
||||
<a v-else :href="`/#/system-eventlog?search=${content.id}`">{{ content.id }}</a>
|
||||
<span v-if="content.stats"> {{ prettyFileSize(content.stats.size) }} - {{ content.stats.fileCount }} file(s)</span>
|
||||
</div>
|
||||
<TableView :columns="backupContentTableColumns" :model="infoBackup.contents" :busy="infoDialogBusy">
|
||||
<template #label="content">
|
||||
<a v-if="content.id === 'mail'" href="/#/mailboxes">{{ content.label }}</a>
|
||||
<a v-else-if="content.fqdn" :href="`/#/app/${content.id}/backups`">{{ content.label || content.fqdn }}</a>
|
||||
<a v-else :href="`/#/system-eventlog?search=${content.id}`">{{ content.id }}</a>
|
||||
</template>
|
||||
<template #fileCount="content">
|
||||
<div v-if="content.stats?.upload" style="text-align: right">{{ content.stats.upload.fileCount }}</div>
|
||||
<div v-else style="text-align: right">-</div>
|
||||
</template>
|
||||
<!-- <td>{{ prettyDuration(content.stats.upload.duration | content.stats.copy.duration) }}</td> -->
|
||||
<template #size="content">
|
||||
<div v-if="content.stats?.upload" style="text-align: right">{{ prettyFileSize(content.stats.upload.size) }}</div>
|
||||
<div v-else style="text-align: right">-</div>
|
||||
</template>
|
||||
</TableView>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="editDialog"
|
||||
@@ -360,7 +403,7 @@ defineExpose({ refresh });
|
||||
</template>
|
||||
|
||||
<template #size="backup">
|
||||
<span v-if="backup.stats?.aggregated">{{ prettyFileSize(backup.stats.aggregated.size) }} - {{ backup.stats.aggregated.fileCount }} file(s)</span>
|
||||
<span v-if="backup.stats?.aggregatedUpload">{{ prettyFileSize(backup.stats.aggregatedUpload.size) }} | {{ backup.stats.aggregatedUpload.fileCount }} file(s)</span>
|
||||
</template>
|
||||
|
||||
<template #site="backup">{{ backup.site.name }}</template>
|
||||
|
||||
@@ -6,39 +6,64 @@ import AccessControl from '../AccessControl.vue';
|
||||
import OperatorAccessControl from '../OperatorAccessControl.vue';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
import { ACL_OPTIONS } from '../../constants.js';
|
||||
import UsersModel from '../../models/UsersModel.js';
|
||||
import GroupsModel from '../../models/GroupsModel.js';
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const usersModel = UsersModel.create();
|
||||
const groupsModel = GroupsModel.create();
|
||||
|
||||
const busy = ref(false);
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
|
||||
const loading = ref(false);
|
||||
const submitBusy = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const accessRestrictionOption = ref(ACL_OPTIONS.ANY);
|
||||
const accessRestrictionAcl = ref({ users: [], groups: [] });
|
||||
const operatorAcl = ref({ users: [], groups: [] });
|
||||
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
submitBusy.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
let [error] = await appsModel.configure(props.app.id, 'access_restriction', { accessRestriction: accessRestrictionOption.value === ACL_OPTIONS.ANY ? null : (accessRestrictionOption.value === ACL_OPTIONS.NOSSO ? false : accessRestrictionAcl.value) });
|
||||
if (error) {
|
||||
errorMessage.value = error.body ? error.body.message : 'Internal error';
|
||||
busy.value = false;
|
||||
submitBusy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
[error] = await appsModel.configure(props.app.id, 'operators', { operators: (operatorAcl.value.users.length || operatorAcl.value.groups.length) ? operatorAcl.value : null});
|
||||
if (error) {
|
||||
errorMessage.value = error.body ? error.body.message : 'Internal error';
|
||||
busy.value = false;
|
||||
submitBusy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
busy.value = false;
|
||||
submitBusy.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
|
||||
let [error, result] = await usersModel.list();
|
||||
if (error) return console.error(error);
|
||||
const userIds = new Set();
|
||||
for (const u of result) {
|
||||
u.username = u.username || u.email; // ensure username
|
||||
userIds.add(u.id);
|
||||
}
|
||||
users.value = result;
|
||||
|
||||
[error, result] = await groupsModel.list();
|
||||
if (error) return console.error(error);
|
||||
groups.value = result;
|
||||
const groupIds = new Set();
|
||||
for (const g of result) groupIds.add(g.id);
|
||||
|
||||
if (props.app.accessRestriction === null) {
|
||||
accessRestrictionOption.value = ACL_OPTIONS.ANY;
|
||||
accessRestrictionAcl.value = { users: [], groups: [] };
|
||||
@@ -47,26 +72,31 @@ onMounted(() => {
|
||||
accessRestrictionAcl.value = { users: [], groups: [] };
|
||||
} else {
|
||||
accessRestrictionOption.value = ACL_OPTIONS.RESTRICTED;
|
||||
accessRestrictionAcl.value = props.app.accessRestriction;
|
||||
accessRestrictionAcl.value = JSON.parse(JSON.stringify(props.app.accessRestriction)); // make a copy
|
||||
accessRestrictionAcl.value.users = accessRestrictionAcl.value.users.filter(uid => userIds.has(uid)); // remove deleted users
|
||||
accessRestrictionAcl.value.groups = accessRestrictionAcl.value.groups.filter(gid => groupIds.has(gid)); // remove deleted groups
|
||||
}
|
||||
|
||||
operatorAcl.value = { users: [], groups: [] };
|
||||
if (props.app.operators) {
|
||||
operatorAcl.value.users = props.app.operators.users;
|
||||
operatorAcl.value.groups = props.app.operators.groups;
|
||||
operatorAcl.value = JSON.parse(JSON.stringify(props.app.operators)); // make a copy
|
||||
operatorAcl.value.users = operatorAcl.value.users.filter(uid => userIds.has(uid)); // remove deleted users
|
||||
operatorAcl.value.groups = operatorAcl.value.groups.filter(gid => groupIds.has(gid)); // remove deleted groups
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!loading">
|
||||
<div class="text-danger" v-if="errorMessage">{{ errorMessage }}</div>
|
||||
<AccessControl v-model:option="accessRestrictionOption" v-model:acl="accessRestrictionAcl" :manifest="app.manifest" :hide-optional-sso-option="!app.sso"/>
|
||||
<AccessControl v-model:option="accessRestrictionOption" v-model:acl="accessRestrictionAcl" :users="users" :groups="groups" :manifest="app.manifest" :hide-optional-sso-option="!app.sso"/>
|
||||
<br/>
|
||||
<OperatorAccessControl v-model:acl="operatorAcl" :has-ftp="app.manifest.addons?.localstorage?.ftp"/>
|
||||
<OperatorAccessControl v-model:acl="operatorAcl" :users="users" :groups="groups" :has-ftp="app.manifest.addons?.localstorage?.ftp"/>
|
||||
<br/>
|
||||
<br/>
|
||||
<Button @click="onSubmit()" :loading="busy" :disabled="busy">{{ $t('main.dialog.save') }}</Button>
|
||||
<Button @click="onSubmit()" :loading="submitBusy" :disabled="submitBusy">{{ $t('main.dialog.save') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -425,7 +425,7 @@ onMounted(async () => {
|
||||
{{ backup.site.name }}
|
||||
</template>
|
||||
<template #size="backup">
|
||||
<span v-if="backup.stats">{{ prettyFileSize(backup.stats.size) }} - {{ backup.stats.fileCount }} file(s)</span>
|
||||
<span v-if="backup.stats?.upload">{{ prettyFileSize(backup.stats.upload.size) }} - {{ backup.stats.upload.fileCount }} file(s)</span>
|
||||
</template>
|
||||
<template #actions="backup">
|
||||
<div style="text-align: right;">
|
||||
|
||||
@@ -81,7 +81,7 @@ onMounted(() => {
|
||||
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('app.updates.info.description') }}</div>
|
||||
<div class="info-value" v-if="app.appStoreId">{{ app.manifest.title }} {{ app.manifest.version }}</div>
|
||||
<div class="info-value" v-if="app.appStoreId">{{ app.manifest.title }} {{ app.manifest.upstreamVersion }}</div>
|
||||
<div class="info-value" v-else>{{ app.manifest.dockerImage }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ref, onMounted, computed, inject } from 'vue';
|
||||
import { Button, SingleSelect, InputGroup, FormGroup, TextInput, Checkbox } from '@cloudron/pankow';
|
||||
import { isValidDomain } from '@cloudron/pankow/utils';
|
||||
import { ISTATES } from '../../constants.js';
|
||||
@@ -13,6 +13,7 @@ const props = defineProps([ 'app' ]);
|
||||
const appsModel = AppsModel.create();
|
||||
const domainsModel = DomainsModel.create();
|
||||
|
||||
const dashboardDomain = inject('dashboardDomain');
|
||||
const domains = ref([]);
|
||||
const busy = ref(false);
|
||||
const errorMessage = ref('');
|
||||
@@ -38,7 +39,7 @@ function isNoopOrManual(domain) {
|
||||
|
||||
function onAddAlias() {
|
||||
aliases.value.push({
|
||||
domain: domains.value[0].domain,
|
||||
domain: dashboardDomain.value,
|
||||
subdomain: ''
|
||||
});
|
||||
}
|
||||
@@ -49,7 +50,7 @@ function onRemoveAlias(index) {
|
||||
|
||||
function onAddRedirect() {
|
||||
redirects.value.push({
|
||||
domain: domains.value[0].domain,
|
||||
domain: dashboardDomain.value,
|
||||
subdomain: ''
|
||||
});
|
||||
}
|
||||
@@ -63,8 +64,16 @@ const formValid = computed(() => {
|
||||
}];
|
||||
|
||||
for (const d in secondaryDomains.value) checkForDomains.push({ domain: secondaryDomains.value[d].domain, subdomain: secondaryDomains.value[d].subdomain });
|
||||
for (const d of aliases.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
|
||||
for (const d of redirects.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
|
||||
for (const d of aliases.value) {
|
||||
let subdomain = d.subdomain;
|
||||
// see apps.js:validateLocations()
|
||||
if (d.subdomain.startsWith('*')) {
|
||||
if (subdomain === '*') continue;
|
||||
subdomain = subdomain.replace(/^\*\./, ''); // remove *.
|
||||
}
|
||||
checkForDomains.push({ domain: d.domain, subdomain: subdomain });
|
||||
}
|
||||
|
||||
if (checkForDomains.find(d => !isValidDomain((d.subdomain ? (d.subdomain + '.') : '') + d.domain))) return false;
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export function createDirectoryModel(origin, accessToken, api) {
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) {
|
||||
if (error.status === 404) return [];
|
||||
if (result.status === 404) return [];
|
||||
|
||||
console.error('Failed to list files', error || result.status);
|
||||
return [];
|
||||
|
||||
@@ -13,6 +13,7 @@ const providers = [
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'Hetzner Cloud', value: 'hetznercloud' },
|
||||
{ name: 'INWX', value: 'inwx' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
@@ -53,6 +54,7 @@ function filterConfigForProvider(provider, config) {
|
||||
props = ['accessToken'];
|
||||
break;
|
||||
case 'hetzner':
|
||||
case 'hetznercloud':
|
||||
props = ['token'];
|
||||
break;
|
||||
case 'vultr':
|
||||
|
||||
@@ -9,7 +9,7 @@ function create() {
|
||||
async list(acknowledged = false) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/notifications`, { acknowledged, access_token: accessToken });
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/notifications`, { acknowledged, access_token: accessToken, per_page: 100 });
|
||||
} catch (e) {
|
||||
return [e];
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ onMounted(async () => {
|
||||
<div class="container">
|
||||
<div class="view">
|
||||
<h1 style="text-align: center;">Welcome to Cloudron</h1>
|
||||
<h3 style="text-align: center;">Set up Admin Account</h3>
|
||||
<h3 style="text-align: center;">Create Admin Account</h3>
|
||||
|
||||
<div class="has-error" v-if="formError.generic">{{ formError.generic }}</div>
|
||||
|
||||
|
||||
@@ -307,8 +307,8 @@ onBeforeUnmount(() => {
|
||||
TODO check if this should be shown on stop confirmation
|
||||
<div>{{ $t('app.uninstall.startStop.description') }}</div>
|
||||
-->
|
||||
<Button v-if="!app.progress && targetRunState() === TARGET_RUN_STATE.START" secondary plain tool icon="fa-solid fa-circle-play" :loading="toggleRunStateBusy" :disabled="toggleRunStateBusy" v-tooltip="$t('app.uninstall.startStop.startAction')" @click="onStartApp()"/>
|
||||
<Button v-else-if="!app.progress" secondary plain tool icon="fa-solid fa-circle-stop" :loading="toggleRunStateBusy" :disabled="toggleRunStateBusy" v-tooltip="$t('app.uninstall.startStop.stopAction')" @click="onStopApp()"/>
|
||||
<Button v-if="!app.progress && targetRunState() === TARGET_RUN_STATE.START" secondary tool icon="fa-solid fa-circle-play" :loading="toggleRunStateBusy" :disabled="toggleRunStateBusy" v-tooltip="$t('app.uninstall.startStop.startAction')" @click="onStartApp()"/>
|
||||
<Button v-else-if="!app.progress" secondary tool icon="fa-solid fa-circle-stop" :loading="toggleRunStateBusy" :disabled="toggleRunStateBusy" v-tooltip="$t('app.uninstall.startStop.stopAction')" @click="onStopApp()"/>
|
||||
|
||||
<ButtonGroup>
|
||||
<Button secondary tool :href="`/logs.html?appId=${app.id}`" target="_blank" v-tooltip="$t('app.logsActionTooltip')" icon="fa-solid fa-align-left" />
|
||||
|
||||
@@ -149,6 +149,11 @@ const filteredApps = computed(() => {
|
||||
if (stateFilter.value === 'update_available') return a.updateInfo;
|
||||
|
||||
return a.runState === RSTATES.RUNNING && (a.health !== HSTATES.HEALTHY || a.installationState !== ISTATES.INSTALLED); // not responding
|
||||
}).sort((a, b) => {
|
||||
const labelA = a.label || a.subdomain || a.fqdn;
|
||||
const labelB = b.label || b.subdomain || b.fqdn;
|
||||
|
||||
return labelA.localeCompare(labelB);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,9 +285,9 @@ onDeactivated(() => {
|
||||
{{ $t('apps.title') }}
|
||||
<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-top: 10px;">
|
||||
<TextInput v-model="filter" :placeholder="$t('apps.searchPlaceholder')" />
|
||||
<SingleSelect class="pankow-no-mobile" v-if="profile.isAtLeastAdmin" :options="tagFilterOptions" option-key="id" option-label="name" v-model="tagFilter" />
|
||||
<SingleSelect class="pankow-no-mobile" v-if="profile.isAtLeastAdmin" :options="stateFilterOptions" option-key="id" v-model="stateFilter" />
|
||||
<SingleSelect class="pankow-no-mobile" v-if="profile.isAtLeastAdmin" :options="domainFilterOptions" option-key="id" option-label="domain" v-model="domainFilter" />
|
||||
<SingleSelect class="pankow-no-mobile" v-if="profile.isAtLeastAdmin" :search-threshold="10" :options="tagFilterOptions" option-key="id" option-label="name" v-model="tagFilter" />
|
||||
<SingleSelect class="pankow-no-mobile" v-if="profile.isAtLeastAdmin" :search-threshold="10" :options="stateFilterOptions" option-key="id" v-model="stateFilter" />
|
||||
<SingleSelect class="pankow-no-mobile" v-if="profile.isAtLeastAdmin" :search-threshold="10" :options="domainFilterOptions" option-key="id" option-label="domain" v-model="domainFilter" />
|
||||
<Button tool outline secondary @click="toggleView()" :icon="viewType === VIEW_TYPE.GRID ? 'fas fa-list' : 'fas fa-grip'"></Button>
|
||||
</div>
|
||||
</h1>
|
||||
@@ -296,7 +301,7 @@ onDeactivated(() => {
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="grid-animation" tag="div" class="grid" v-if="viewType === VIEW_TYPE.GRID">
|
||||
<a v-for="app in filteredApps" :key="app.id" class="grid-item" @click="onOpenApp(app, $event)" :href="'https://' + app.fqdn" target="_blank" :style="{ width: itemWidth }">
|
||||
<a v-for="app in filteredApps" :key="app.id" class="grid-item" :class="{ 'item-inactive': app.runState === RSTATES.STOPPED }" @click="onOpenApp(app, $event)" :href="'https://' + app.fqdn" target="_blank" :style="{ width: itemWidth }">
|
||||
<img :alt="app.label || app.subdomain || app.fqdn" :src="app.iconUrl" v-fallback-image="API_ORIGIN + '/img/appicon_fallback.png'"/>
|
||||
<div class="grid-item-label" v-fit-text>{{ app.label || app.subdomain || app.fqdn }}</div>
|
||||
<div class="grid-item-task-label" v-if="app.type === APP_TYPES.LINK">{{ $t('app.appLink.title') }}</div>
|
||||
@@ -317,7 +322,7 @@ onDeactivated(() => {
|
||||
<TableView :columns="listColumns" :model="filteredApps">
|
||||
<template #icon="app">
|
||||
<a :href="'https://' + app.fqdn" target="_blank">
|
||||
<img :alt="app.label || app.subdomain || app.fqdn" class="list-icon" :src="app.iconUrl" v-fallback-image="API_ORIGIN + '/img/appicon_fallback.png'"/>
|
||||
<img :alt="app.label || app.subdomain || app.fqdn" class="list-icon" :class="{ 'item-inactive': app.runState === RSTATES.STOPPED }" :src="app.iconUrl" v-fallback-image="API_ORIGIN + '/img/appicon_fallback.png'"/>
|
||||
</a>
|
||||
</template>
|
||||
<template #label="app">
|
||||
@@ -402,6 +407,10 @@ tr:hover .action-button {
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
.item-inactive {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.list-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
@@ -204,6 +204,8 @@ onActivated(async () => {
|
||||
onHashChange();
|
||||
|
||||
window.addEventListener('resize', setItemWidth);
|
||||
|
||||
setTimeout(() => searchInput.value.focus(), 0);
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
@@ -223,7 +225,7 @@ onDeactivated(() => {
|
||||
|
||||
<div class="filter-bar">
|
||||
<SingleSelect v-model="category" :options="categories" option-key="id" option-label="label" :disabled="!ready"/>
|
||||
<TextInput ref="searchInput" @keydown.esc="search = ''" v-model="search" :disabled="!ready" :placeholder="$t('appstore.searchPlaceholder')" style="flex-grow: 1;"/>
|
||||
<TextInput ref="searchInput" @keydown.esc="search = ''" v-model="search" :disabled="!ready" :placeholder="$t('appstore.searchPlaceholder')" style="flex-grow: 1;" autocomplete="off"/>
|
||||
</div>
|
||||
|
||||
<div v-if="!ready" style="margin-top: 15px">
|
||||
|
||||
@@ -5,7 +5,7 @@ const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef, reactive, inject } from 'vue';
|
||||
import { Button, Menu, ProgressBar, InputDialog, Spinner } from '@cloudron/pankow';
|
||||
import { Button, Menu, ProgressBar, InputDialog } from '@cloudron/pankow';
|
||||
import { prettyLongDate } from '@cloudron/pankow/utils';
|
||||
import Section from '../components/Section.vue';
|
||||
import StateLED from '../components/StateLED.vue';
|
||||
@@ -65,8 +65,13 @@ function prettyBackupSchedule(pattern) {
|
||||
prettyDay = days.map(function (day) { return cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(',');
|
||||
}
|
||||
|
||||
const prettyHour = hours.map(function (hour) { return cronHours[parseInt(hour, 10)].name; }).join(',');
|
||||
return prettyDay + ' at ' + prettyHour;
|
||||
let prettyHour;
|
||||
if (hours.length === 24 || hours[0] === '*') {
|
||||
prettyHour = 'hourly';
|
||||
} else {
|
||||
prettyHour = hours.map(function (hour) { return cronHours[parseInt(hour, 10)].name; }).join(',');
|
||||
}
|
||||
return prettyDay + ' @ ' + prettyHour;
|
||||
};
|
||||
|
||||
function prettyBackupRetention(retention) {
|
||||
|
||||
@@ -131,10 +131,10 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="domain.inboundEnabled">
|
||||
Outbound (via {{ prettyRelayProviderName(domain.relayProvider) }}) - {{ $t('emails.domains.stats', { mailboxCount: domain.mailboxCount, usage: prettyDecimalSize(domain.usage) }) }}
|
||||
{{ $t('emails.domains.inbound') }}. Relayed via {{ prettyRelayProviderName(domain.relayProvider) }}. {{ $t('emails.domains.stats', { mailboxCount: domain.mailboxCount, usage: prettyDecimalSize(domain.usage) }) }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<span v-if="domain.outboundEnabled">{{ $t('emails.domains.outbound') }} (via {{ prettyRelayProviderName(domain.relayProvider) }})</span>
|
||||
<span v-if="domain.outboundEnabled">{{ $t('emails.domains.outbound') }}. Relayed via {{ prettyRelayProviderName(domain.relayProvider) }}</span>
|
||||
<span v-else>{{ $t('emails.domains.disabled') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -230,7 +230,7 @@ onMounted(async () => {
|
||||
|
||||
<Section :title="$t('email.incoming.mailboxes.title')">
|
||||
<template #header-title-extra>
|
||||
<span style="font-weight: normal; font-size: 14px">({{ $t('emails.domains.stats', { mailboxCount: filteredMailboxes.length, usage: prettyDecimalSize(filteredMailboxesUsage) }) }})</span>
|
||||
<span style="font-weight: normal; font-size: 14px">({{ $t('email.incoming.mailboxes.stats', { mailboxCount: filteredMailboxes.length, usage: prettyDecimalSize(filteredMailboxesUsage) }) }})</span>
|
||||
</template>
|
||||
<template #header-buttons>
|
||||
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="searchFilter"/>
|
||||
|
||||
@@ -132,7 +132,7 @@ onMounted(async () => {
|
||||
</fieldset>
|
||||
|
||||
<div class="actions">
|
||||
<Button id="totpTokenSubmitButton" @click="onSubmit" :disabled="busy || !totpToken" :loading="busy">{{ $t('login.signInAction') }}</Button>
|
||||
<Button id="totpTokenSubmitButton" @click="onSubmit" :disabled="busy || !totpToken" :loading="busy">{{ $t('login.loginAction') }}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -151,9 +151,14 @@ onMounted(async () => {
|
||||
|
||||
[error, result] = await cloudronModel.languages();
|
||||
languages.value = result.map(l => {
|
||||
const displayNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
||||
|
||||
// hack for chinese simplified
|
||||
l = l === 'zh_Hans' ? 'zh' : l;
|
||||
|
||||
return {
|
||||
id: l,
|
||||
display: t(`lang.${l}`, l /* default fallback */, { locale: 'en' })
|
||||
display: displayNames.of(l) || l,
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
return a.display.localeCompare(b.display);
|
||||
|
||||
@@ -16,7 +16,7 @@ const loginUrl = window.cloudron.loginUrl;
|
||||
<small>{{ $t('login.loginTo') }}</small>
|
||||
<h1>{{ name }}</h1>
|
||||
<br/>
|
||||
<Button id="loginProceedButton" :href="loginUrl">{{ $t('login.signInAction') }}</Button>
|
||||
<Button id="loginProceedButton" :href="loginUrl">{{ $t('login.loginAction') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -149,7 +149,7 @@ onMounted(async () => {
|
||||
// aws marketplace made a policy change that they one cannot provide route53 IAM credentials
|
||||
provider.value = 'wildcard';
|
||||
} else { // some default to make the form not feel "empty"
|
||||
provider.value = 'hetzner';
|
||||
provider.value = 'digitalocean';
|
||||
}
|
||||
|
||||
const [error2, result] = await provisionModel.detectIp();
|
||||
|
||||
@@ -58,9 +58,14 @@ onMounted(async () => {
|
||||
if (error) return console.error(error);
|
||||
|
||||
allLanguages.value = result.map(l => {
|
||||
const displayNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
||||
|
||||
// hack for chinese simplified
|
||||
l = l === 'zh_Hans' ? 'zh' : l;
|
||||
|
||||
return {
|
||||
id: l,
|
||||
display: t(`lang.${l}`, l /* default fallback */, { locale: 'en' })
|
||||
display: displayNames.of(l) || l,
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
return a.display.localeCompare(b.display);
|
||||
|
||||
@@ -50,7 +50,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<Section :title="$t('users.title')" :title-badge="!features.profileConfig ? 'Upgrade' : ''">
|
||||
<Section :title="$t('users.settings.title')" :title-badge="!features.profileConfig ? 'Upgrade' : ''">
|
||||
<SettingsItem>
|
||||
<div>{{ $t('users.settings.allowProfileEditCheckbox') }} <sup><a href="https://docs.cloudron.io/user-directory/#lock-profile" target="_blank"><i class="fa fa-question-circle"></i></a></sup></div>
|
||||
<Switch v-model="editableUserProfiles" @change="onToggleEditableUserProfiles" :disabled="busy || !features.profileConfig"/>
|
||||
|
||||
@@ -101,7 +101,7 @@ exports.up = async function (db) {
|
||||
|
||||
await db.runSql('START TRANSACTION');
|
||||
await db.runSql('INSERT INTO backupSites (id, name, provider, configJson, limitsJson, integrityKeyPairJson, retentionJson, schedule, encryptionJson, format, enableForUpdates) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ id, name, provider, JSON.stringify(config), JSON.stringify(limits), JSON.stringify(integrityKeyPair), JSON.stringify(retention), schedule, JSON.stringify(encryption), format, enableForUpdates ]);
|
||||
[ id, name, provider, JSON.stringify(config), JSON.stringify(limits), JSON.stringify(integrityKeyPair), JSON.stringify(retention), schedule, encryption ? JSON.stringify(encryption) : null, format, enableForUpdates ]);
|
||||
await deleteOldSettings(db);
|
||||
await db.runSql('UPDATE tasks SET type=? WHERE type=?', [ `backup_${id}`, 'backup' ]); // migrate the tasks
|
||||
await db.runSql('COMMIT');
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
const crypto = require('node:crypto'),
|
||||
path = require('node:path'),
|
||||
paths = require('../src/paths.js');
|
||||
paths = require('../src/paths.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
exports.up = async function(db) {
|
||||
const backups = await db.runSql('SELECT format, COUNT(*) AS count FROM backups GROUP BY format WITH ROLLUP', []); // https://dev.mysql.com/doc/refman/8.4/en/group-by-modifiers.html
|
||||
@@ -39,7 +40,7 @@ exports.up = async function(db) {
|
||||
await db.runSql('ALTER TABLE backups ADD siteId VARCHAR(128)');
|
||||
if (totalCount) {
|
||||
if (currentBackupSite.format === 'tgz') {
|
||||
const ext = currentBackupSite.encryptionJson ? '.tar.gz.enc' : '.tar.gz';
|
||||
const ext = safe.JSON.parse(currentBackupSite.encryptionJson) ? '.tar.gz.enc' : '.tar.gz';
|
||||
console.log(`Adjusting remotePath of existing tgz backups with ${ext}`);
|
||||
await db.runSql('UPDATE backups SET remotePath=CONCAT(remotePath, ?) WHERE format=?', [ ext, 'tgz' ]);
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -12,7 +12,7 @@
|
||||
"@aws-sdk/client-s3": "^3.901.0",
|
||||
"@aws-sdk/lib-storage": "^3.901.0",
|
||||
"@cloudron/connect-lastmile": "^2.3.0",
|
||||
"@cloudron/manifest-format": "^5.28.0",
|
||||
"@cloudron/manifest-format": "^5.29.0",
|
||||
"@cloudron/superagent": "^1.0.0",
|
||||
"@google-cloud/dns": "^5.3.0",
|
||||
"@google-cloud/storage": "^7.17.1",
|
||||
@@ -1141,9 +1141,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cloudron/manifest-format": {
|
||||
"version": "5.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/manifest-format/-/manifest-format-5.28.0.tgz",
|
||||
"integrity": "sha512-XunisSYvpZgdI2i489nRXr6pLa5JYJj95xIoULOSqt0rf2LSbo70x1hGEujuTFS8LXnv0UMu3UFhEXf5exPdzg==",
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/manifest-format/-/manifest-format-5.29.0.tgz",
|
||||
"integrity": "sha512-F0+pZ/ibs6jZAEXa0mKQBcFMLG4zmz4Qjkdx8irM4/1kbkIcvKTBaU1oRt6Uz8F7LSvlc1/D5sK45JKEkrNlCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron": "^4.3.1",
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
"@aws-sdk/client-s3": "^3.901.0",
|
||||
"@aws-sdk/lib-storage": "^3.901.0",
|
||||
"@cloudron/connect-lastmile": "^2.3.0",
|
||||
"@cloudron/manifest-format": "^5.28.0",
|
||||
"@cloudron/manifest-format": "^5.29.0",
|
||||
"@cloudron/superagent": "^1.0.0",
|
||||
"@google-cloud/dns": "^5.3.0",
|
||||
"@google-cloud/storage": "^7.17.1",
|
||||
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const superagent = require('@cloudron/superagent');
|
||||
|
||||
const translations = require('../dashboard/public/translation/en.json');
|
||||
|
||||
// get token from https://translate.cloudron.io/accounts/profile/#api
|
||||
const token = '';
|
||||
|
||||
const flat = flattenObject(translations);
|
||||
const keys = Object.keys(flat);
|
||||
|
||||
function flattenObject(obj, prefix = '', result = {}) {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||
// Rekursiver Aufruf für verschachtelte Objekte
|
||||
flattenObject(obj[key], newKey, result);
|
||||
} else {
|
||||
// Wert direkt zuweisen
|
||||
result[newKey] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function listFiles(dir) {
|
||||
const files = await fs.promises.readdir(dir);
|
||||
let result = [];
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = await fs.promises.stat(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
result = result.concat(await listFiles(fullPath));
|
||||
} else if (stat.isFile()) {
|
||||
result.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function grepFile(fullPath, searchString) {
|
||||
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
||||
return content.includes(searchString);
|
||||
}
|
||||
|
||||
async function findDuplicates() {
|
||||
const duplicates = [];
|
||||
for (const key of keys) {
|
||||
const value = flat[key];
|
||||
const dups = keys.filter(k => flat[k] === value && k !== key);
|
||||
if (dups.length === 0) continue;
|
||||
|
||||
// skip double entries
|
||||
if (duplicates.find(d => d.value === value)) continue;
|
||||
|
||||
duplicates.push({ key, value, dups });
|
||||
}
|
||||
|
||||
console.log('Keys with the same values multiple times:');
|
||||
console.log('----------------------------------------------------------');
|
||||
duplicates.forEach(d => console.log(`${d.key}\n\t${d.dups.join(' \n\t')} \n => "${d.value}"\n `));
|
||||
console.log('----------------------------------------------------------');
|
||||
console.log(' Count: ' + duplicates.length);
|
||||
console.log('----------------------------------------------------------\n\n');
|
||||
}
|
||||
|
||||
async function findUnused() {
|
||||
const keyUsage = {};
|
||||
let files = await listFiles(path.resolve('./dashboard/src/'));
|
||||
files = files.concat(await listFiles(path.resolve('./src/')));
|
||||
|
||||
for (const file of files) {
|
||||
const content = await fs.promises.readFile(file, 'utf-8');
|
||||
|
||||
for (const key of keys) {
|
||||
if (!keyUsage[key]) keyUsage[key] = 0;
|
||||
if (content.includes(key)) keyUsage[key]++;
|
||||
}
|
||||
}
|
||||
const unusedKeys = Object.keys(keyUsage).filter(k => keyUsage[k] === 0)
|
||||
|
||||
console.log('Unused keys:');
|
||||
console.log('----------------------------------------------------------');
|
||||
console.log(unusedKeys.join('\n'));
|
||||
console.log('----------------------------------------------------------');
|
||||
console.log(' Count: ' + unusedKeys.length);
|
||||
console.log('----------------------------------------------------------\n\n');
|
||||
|
||||
return unusedKeys;
|
||||
}
|
||||
|
||||
async function purge(key) {
|
||||
let result = await superagent.get('https://translate.cloudron.io/api/units/')
|
||||
.set('Authorization', `Token ${token}`)
|
||||
.query({q: `language:en AND component:dashboard AND key:${key}`});
|
||||
|
||||
const id = result.body.results[0].id;
|
||||
|
||||
console.log(` => deleting ${key} with id ${id}`);
|
||||
await superagent.del(`https://translate.cloudron.io/api/units/${id}/`)
|
||||
.set('Authorization', `Token ${token}`);
|
||||
|
||||
console.log(' done');
|
||||
}
|
||||
|
||||
(async () => {
|
||||
|
||||
console.log(`Found ${keys.length} strings`);
|
||||
console.log('----------------------------------------------------------\n\n');
|
||||
|
||||
console.log('Toplevel keys to be moved:');
|
||||
console.log('----------------------------------------------------------');
|
||||
console.log(Object.keys(flat).find(k => k.indexOf('.') === -1));
|
||||
console.log('----------------------------------------------------------\n\n');
|
||||
|
||||
await findDuplicates();
|
||||
// const unused = await findUnused();
|
||||
|
||||
// for (const key of unused) {
|
||||
// await purge(key);
|
||||
// }
|
||||
|
||||
})();
|
||||
@@ -346,6 +346,8 @@ async function registerCloudron3() {
|
||||
async function unlinkAccount() {
|
||||
debug('unlinkAccount: Unlinking existing account.');
|
||||
|
||||
if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
|
||||
|
||||
await unregister();
|
||||
return await registerCloudron3();
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ async function cleanupAppBackups(site, referencedBackupIds, progressCallback) {
|
||||
const allAppIds = allApps.map(a => a.id);
|
||||
|
||||
// high number, try to get all app backups as we had a cloudron with over 100 apps with 4 daily backups for one month!
|
||||
const appBackups = await backups.listByTypePaged(backups.BACKUP_TYPE_APP, 1, 100000);
|
||||
const appBackups = await backups.listByTypePaged(backups.BACKUP_TYPE_APP, site.id, 1, 100000);
|
||||
|
||||
// collate the backups by app id. note that the app could already have been uninstalled
|
||||
const appBackupsById = {};
|
||||
@@ -155,7 +155,7 @@ async function cleanupMailBackups(site, referencedBackupIds, progressCallback) {
|
||||
|
||||
const removedMailBackupPaths = [];
|
||||
|
||||
const mailBackups = await backups.listByTypePaged(backups.BACKUP_TYPE_MAIL, 1, 100000);
|
||||
const mailBackups = await backups.listByTypePaged(backups.BACKUP_TYPE_MAIL, site.id, 1, 100000);
|
||||
|
||||
applyBackupRetention(mailBackups, Object.assign({ keepLatest: true }, site.retention), referencedBackupIds);
|
||||
|
||||
@@ -180,7 +180,7 @@ async function cleanupBoxBackups(site, progressCallback) {
|
||||
// We need to fetch all box backups to be able to compile a list of all referenced app backupSites.
|
||||
// Otherwise if we miss some app backups, they will get purged!
|
||||
// 100000 here should be seen as infinity
|
||||
const boxBackups = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, 1, 100000);
|
||||
const boxBackups = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, site.id, 1, 100000);
|
||||
|
||||
applyBackupRetention(boxBackups, Object.assign({ keepLatest: true }, site.retention), [] /* references */);
|
||||
|
||||
@@ -213,7 +213,7 @@ async function cleanupMissingBackups(backupSite, progressCallback) {
|
||||
|
||||
let page = 1, result = [];
|
||||
do {
|
||||
result = await backups.list(page, perPage);
|
||||
result = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, backupSite.id, page, perPage);
|
||||
|
||||
for (const backup of result) {
|
||||
if (backup.state !== backups.BACKUP_STATE_NORMAL) continue; // note: errored and incomplete backups are cleaned up by the backup retention logic
|
||||
|
||||
@@ -71,7 +71,7 @@ async function addFile(sourceFile, encryption, uploader, progressCallback) {
|
||||
await uploader.finish();
|
||||
|
||||
return {
|
||||
stats: ps.stats(), // { startTime, totalMsecs, transferred }
|
||||
stats: { transferred: ps.stats().transferred },
|
||||
integrity: { size: ps.stats().transferred, sha256: hash.digest('hex') }
|
||||
};
|
||||
}
|
||||
@@ -91,8 +91,6 @@ async function sync(backupSite, remotePath, dataLayout, progressCallback) {
|
||||
transferred: 0,
|
||||
size: [...integrityMap.values()].reduce((sum, integrity) => sum + (integrity?.size || 0), 0), // integrity can be null if file had disappeared during upload
|
||||
fileCount: addQueue.length + integrityMap.size, // final file count, not the transferred file count
|
||||
startTime: Date.now(),
|
||||
totalMsecs: 0
|
||||
};
|
||||
|
||||
const destPathIntegrityMap = new Map(); // unlike integrityMap which contains local filenames, this contains destination filenames (maybe encrypted)
|
||||
@@ -138,7 +136,7 @@ async function sync(backupSite, remotePath, dataLayout, progressCallback) {
|
||||
await syncer.finalize(integrityMap, cacheFile);
|
||||
|
||||
return {
|
||||
stats: { ...aggregatedStats, totalMsecs: Date.now()-aggregatedStats.startTime },
|
||||
stats: aggregatedStats,
|
||||
integrityMap: destPathIntegrityMap
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,12 +170,12 @@ async function tarPack(dataLayout, encryption, uploader, progressCallback) {
|
||||
const [error] = await pipeline; // already wrapped in safe()
|
||||
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, `tarPack pipeline error: ${error.message}`);
|
||||
|
||||
const stats = ps.stats();
|
||||
const stats = ps.stats(); // { startTime, totalMsecs, transferred }
|
||||
debug(`tarPack: pipeline finished: ${JSON.stringify(stats)}`);
|
||||
|
||||
await uploader.finish();
|
||||
return {
|
||||
stats: { fileCount, size: stats.transferred, ...stats },
|
||||
stats: { fileCount, size: stats.transferred, transferred: stats.transferred },
|
||||
integrity: { size: stats.transferred, fileCount, sha256: hash.digest('hex') }
|
||||
};
|
||||
}
|
||||
|
||||
+6
-36
@@ -6,9 +6,6 @@ exports = module.exports = {
|
||||
getLatestInTargetByIdentifier, // brutal function name
|
||||
add,
|
||||
update,
|
||||
setState,
|
||||
list,
|
||||
listBySiteId,
|
||||
listByTypePaged,
|
||||
del,
|
||||
|
||||
@@ -147,9 +144,12 @@ async function update(backup, data) {
|
||||
|
||||
const fields = [], values = [];
|
||||
for (const p in data) {
|
||||
if (p === 'label' || p === 'preserveSecs') {
|
||||
if (p === 'label' || p === 'preserveSecs' || p === 'state') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(data[p]);
|
||||
} else if (p === 'stats') {
|
||||
fields.push(`${p}Json=?`);
|
||||
values.push(JSON.stringify(data[p]));
|
||||
}
|
||||
}
|
||||
values.push(backup.id);
|
||||
@@ -165,43 +165,13 @@ async function update(backup, data) {
|
||||
}
|
||||
}
|
||||
|
||||
async function setState(id, state) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof state, 'string');
|
||||
|
||||
const result = await database.query('UPDATE backups SET state = ? WHERE id = ?', [state, id]);
|
||||
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Backup not found');
|
||||
}
|
||||
|
||||
async function list(page, perPage) {
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
|
||||
const results = await database.query(`SELECT ${BACKUPS_FIELDS} FROM backups ORDER BY creationTime DESC LIMIT ?,?`, [ (page-1)*perPage, perPage ]);
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function listByTypePaged(type, page, perPage) {
|
||||
async function listByTypePaged(type, siteId, page, perPage) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
|
||||
const results = await database.query(`SELECT ${BACKUPS_FIELDS} FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?`, [ type, (page-1)*perPage, perPage ]);
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function listBySiteId(siteId, page, perPage) {
|
||||
assert.strictEqual(typeof siteId, 'string');
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
|
||||
const results = await database.query(`SELECT ${BACKUPS_FIELDS} FROM backups WHERE siteId=? AND type=? ORDER BY creationTime DESC LIMIT ?,?`, [ siteId, exports.BACKUP_TYPE_BOX, (page-1)*perPage, perPage ]);
|
||||
const results = await database.query(`SELECT ${BACKUPS_FIELDS} FROM backups WHERE siteId=? AND type = ? ORDER BY creationTime DESC LIMIT ?,?`, [ siteId, type, (page-1)*perPage, perPage ]);
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
|
||||
+43
-23
@@ -106,11 +106,12 @@ async function upload(remotePath, siteId, dataLayoutString, progressCallback) {
|
||||
// - tgz: only one entry named "." in the map. fileCount has the file count inside.
|
||||
// - rsync: entry for each relative path.
|
||||
// integrity - { signature } of the uploaded .backupinfo .
|
||||
// stats - { fileCount, size, startTime, totalMsecs, transferred }
|
||||
// stats - { fileCount, size, transferred }
|
||||
// - tgz: size (backup size) and transferred is the same
|
||||
// - rsync: size (final backup size) will be different from what was transferred (only changed files)
|
||||
// stats.fileCount and stats.size are stored in db and should match up what is written into .backupinfo
|
||||
const { stats, integrityMap } = await backupFormats.api(backupSite.format).upload(backupSite, remotePath, dataLayout, progressCallback);
|
||||
debug(`upload: path ${remotePath} site ${siteId} uploaded: ${JSON.stringify(stats)}`);
|
||||
|
||||
progressCallback({ message: `Uploading integrity information to ${remotePath}.backupinfo` });
|
||||
const signature = await uploadBackupInfo(backupSite, remotePath, integrityMap);
|
||||
@@ -203,7 +204,7 @@ async function runBackupUpload(uploadConfig, progressCallback) {
|
||||
throw new BoxError(BoxError.EXTERNAL_ERROR, lastMessage.errorMessage);
|
||||
}
|
||||
|
||||
return lastMessage.result;
|
||||
return lastMessage.result; // { stats, integrity }
|
||||
}
|
||||
|
||||
async function snapshotBox(progressCallback) {
|
||||
@@ -272,25 +273,27 @@ async function copy(backupSite, srcRemotePath, destRemotePath, progressCallback)
|
||||
|
||||
async function backupBox(backupSite, appBackupsMap, tag, options, progressCallback) {
|
||||
assert.strictEqual(typeof backupSite, 'object');
|
||||
assert(util.types.isMap(appBackupsMap), 'integrityMap should be a Map'); // id -> stats { fileCount, size, startTime, totalMsecs, transferred }
|
||||
assert(util.types.isMap(appBackupsMap), 'appBackupsMap should be a Map'); // id -> stats: { upload: { fileCount, size, startTime, duration, transferred } }
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
|
||||
const { stats, integrity } = await uploadBoxSnapshot(backupSite, progressCallback);
|
||||
const uploadStartTime = Date.now();
|
||||
const uploadResult = await uploadBoxSnapshot(backupSite, progressCallback); // { stats, integrity }
|
||||
const stats = { upload: { ...uploadResult.stats, startTime: uploadStartTime, duration: Date.now() - uploadStartTime } };
|
||||
|
||||
const remotePath = addFileExtension(backupSite, `${tag}/box_v${constants.VERSION}`);
|
||||
|
||||
debug(`backupBox: rotating box snapshot of ${backupSite.id} to id ${remotePath}`);
|
||||
|
||||
// stats object might be null for stopped/errored apps from old versions
|
||||
stats.aggregated = Array.from(appBackupsMap.values()).filter(s => !!s).reduce((acc, s) => ({
|
||||
fileCount: acc.fileCount + s.fileCount,
|
||||
size: acc.size + s.size,
|
||||
startTime: Math.min(acc.startTime, s.startTime),
|
||||
totalMsecs: acc.totalMsecs + s.totalMsecs,
|
||||
transferred: acc.transferred + s.transferred,
|
||||
}), stats);
|
||||
stats.aggregatedUpload = Array.from(appBackupsMap.values()).filter(s => !!s?.upload).reduce((acc, cur) => ({
|
||||
fileCount: acc.fileCount + cur.upload.fileCount,
|
||||
size: acc.size + cur.upload.size,
|
||||
transferred: acc.transferred + cur.upload.transferred,
|
||||
startTime: Math.min(acc.startTime, cur.upload.startTime),
|
||||
duration: acc.duration + cur.upload.duration,
|
||||
}), stats.upload);
|
||||
|
||||
debug(`backupBox: rotating box snapshot of ${backupSite.id} to id ${remotePath}. ${JSON.stringify(stats)}`);
|
||||
|
||||
const data = {
|
||||
remotePath,
|
||||
@@ -305,14 +308,23 @@ async function backupBox(backupSite, appBackupsMap, tag, options, progressCallba
|
||||
appConfig: null,
|
||||
siteId: backupSite.id,
|
||||
stats,
|
||||
integrity
|
||||
integrity: uploadResult.integrity
|
||||
};
|
||||
|
||||
const id = await backups.add(data);
|
||||
const snapshotPath = addFileExtension(backupSite, 'snapshot/box');
|
||||
const copyStartTime = Date.now();
|
||||
const [error] = await safe(copy(backupSite, snapshotPath, remotePath, progressCallback));
|
||||
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
|
||||
await backups.setState(id, state);
|
||||
if (!error) {
|
||||
stats.copy = { startTime: copyStartTime, duration: Date.now() - copyStartTime };
|
||||
// stats object might be null for stopped/errored apps from old versions
|
||||
stats.aggregatedCopy = Array.from(appBackupsMap.values()).filter(s => !!s).reduce((acc, cur) => ({
|
||||
startTime: Math.min(acc.startTime, cur.copy.startTime),
|
||||
duration: acc.duration + cur.copy.duration,
|
||||
}), stats.copy);
|
||||
}
|
||||
await backups.update({ id }, { stats, state });
|
||||
if (error) throw error;
|
||||
|
||||
return id;
|
||||
@@ -377,7 +389,9 @@ async function backupAppWithTag(app, backupSite, tag, options, progressCallback)
|
||||
return { id: lastKnownGoodAppBackup.id, stats: lastKnownGoodAppBackup.stats };
|
||||
}
|
||||
|
||||
const { stats, integrity } = await uploadAppSnapshot(backupSite, app, progressCallback);
|
||||
const uploadStartTime = Date.now();
|
||||
const uploadResult = await uploadAppSnapshot(backupSite, app, progressCallback); // { stats, integrity }
|
||||
const stats = { upload: { ...uploadResult.stats, startTime: uploadStartTime, duration: Date.now() - uploadStartTime } };
|
||||
|
||||
const manifest = app.manifest;
|
||||
const remotePath = addFileExtension(backupSite, `${tag}/app_${app.fqdn}_v${manifest.version}`);
|
||||
@@ -397,17 +411,19 @@ async function backupAppWithTag(app, backupSite, tag, options, progressCallback)
|
||||
appConfig: app,
|
||||
siteId: backupSite.id,
|
||||
stats,
|
||||
integrity
|
||||
integrity: uploadResult.integrity
|
||||
};
|
||||
|
||||
const id = await backups.add(data);
|
||||
const snapshotPath = addFileExtension(backupSite, `snapshot/app_${app.id}`);
|
||||
const copyStartTime = Date.now();
|
||||
const [error] = await safe(copy(backupSite, snapshotPath, remotePath, progressCallback));
|
||||
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
|
||||
await backups.setState(id, state);
|
||||
if (!error) stats.copy = { startTime: copyStartTime, duration: Date.now() - copyStartTime };
|
||||
await backups.update({ id }, { stats, state });
|
||||
if (error) throw error;
|
||||
|
||||
return { id, stats: data.stats };
|
||||
return { id, stats };
|
||||
}
|
||||
|
||||
async function backupApp(app, backupSite, options, progressCallback) {
|
||||
@@ -466,7 +482,9 @@ async function backupMailWithTag(backupSite, tag, options, progressCallback) {
|
||||
|
||||
debug(`backupMailWithTag: backing up mail with tag ${tag}`);
|
||||
|
||||
const { stats, integrity } = await uploadMailSnapshot(backupSite, progressCallback);
|
||||
const uploadStartTime = Date.now();
|
||||
const uploadResult = await uploadMailSnapshot(backupSite, progressCallback); // { stats, integrity }
|
||||
const stats = { upload: { ...uploadResult.stats, startTime: uploadStartTime, duration: Date.now() - uploadStartTime } };
|
||||
|
||||
const remotePath = addFileExtension(backupSite, `${tag}/mail_v${constants.VERSION}`);
|
||||
|
||||
@@ -485,17 +503,19 @@ async function backupMailWithTag(backupSite, tag, options, progressCallback) {
|
||||
appConfig: null,
|
||||
siteId: backupSite.id,
|
||||
stats,
|
||||
integrity
|
||||
integrity: uploadResult.integrity
|
||||
};
|
||||
|
||||
const id = await backups.add(data);
|
||||
const snapshotPath = addFileExtension(backupSite, 'snapshot/mail');
|
||||
const copyStartTime = Date.now();
|
||||
const [error] = await safe(copy(backupSite, snapshotPath, remotePath, progressCallback));
|
||||
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
|
||||
await backups.setState(id, state);
|
||||
if (!error) stats.copy = { startTime: copyStartTime, duration: Date.now() - copyStartTime };
|
||||
await backups.update({ id }, { stats, state });
|
||||
if (error) throw error;
|
||||
|
||||
return { id, stats: data.stats };
|
||||
return { id, stats };
|
||||
}
|
||||
|
||||
async function downloadMail(backupSite, remotePath, progressCallback) {
|
||||
|
||||
@@ -60,6 +60,7 @@ function api(provider) {
|
||||
case 'namecheap': return require('./dns/namecheap.js');
|
||||
case 'netcup': return require('./dns/netcup.js');
|
||||
case 'hetzner': return require('./dns/hetzner.js');
|
||||
case 'hetznercloud': return require('./dns/hetznercloud.js');
|
||||
case 'noop': return require('./dns/noop.js');
|
||||
case 'manual': return require('./dns/manual.js');
|
||||
case 'ovh': return require('./dns/ovh.js');
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
removePrivateFields,
|
||||
injectPrivateFields,
|
||||
upsert,
|
||||
get,
|
||||
del,
|
||||
wait,
|
||||
verifyDomainConfig
|
||||
};
|
||||
|
||||
const assert = require('node:assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/hetznercloud'),
|
||||
dig = require('../dig.js'),
|
||||
dns = require('../dns.js'),
|
||||
promiseRetry = require('../promise-retry.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('@cloudron/superagent'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
// https://docs.hetzner.cloud/reference/cloud
|
||||
|
||||
const ENDPOINT = 'https://api.hetzner.cloud/v1';
|
||||
|
||||
function formatError(response) {
|
||||
return `Hetzner DNS error ${response.status} ${response.text}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
delete domainObject.config.token;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (!Object.hasOwn(newConfig, 'token')) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
async function getZone(domainConfig, zoneName) {
|
||||
assert.strictEqual(typeof domainConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
|
||||
const [error, response] = await safe(superagent.get(`${ENDPOINT}/zones`)
|
||||
.set('Authorization', `Bearer ${domainConfig.token}`)
|
||||
.query({ search_name: zoneName })
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
|
||||
if (!Array.isArray(response.body.zones)) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
|
||||
const zone = response.body.zones.find(z => z.name === zoneName);
|
||||
if (!zone) throw new BoxError(BoxError.NOT_FOUND, formatError(response));
|
||||
return zone;
|
||||
}
|
||||
|
||||
async function getRecords(domainConfig, zone, name, type) {
|
||||
assert.strictEqual(typeof domainConfig, 'object');
|
||||
assert.strictEqual(typeof zone, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
|
||||
debug(`getRecords: getting dns records of ${zone.name} with ${name} and type ${type}`);
|
||||
|
||||
const [error, response] = await safe(superagent.get(`${ENDPOINT}/zones/${zone.id}/rrsets/${name}/${type.toUpperCase()}`)
|
||||
.set('Authorization', `Bearer ${domainConfig.token}`)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 404) return [];
|
||||
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
|
||||
return response.body.rrset.records;
|
||||
}
|
||||
|
||||
async function waitForAction(domainConfig, id) {
|
||||
assert.strictEqual(typeof domainConfig, 'object');
|
||||
assert.strictEqual(typeof id, 'number');
|
||||
|
||||
await promiseRetry({ times: 100, interval: 1000, debug }, async () => {
|
||||
const [error, response] = await safe(superagent.get(`${ENDPOINT}/actions/${id}`)
|
||||
.set('Authorization', `Bearer ${domainConfig.token}`)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 404) return [];
|
||||
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
if (response.body.action.status !== 'success') throw new BoxError(BoxError.TRY_AGAIN, 'action not done yet');
|
||||
});
|
||||
}
|
||||
|
||||
async function upsert(domainObject, location, type, values) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
|
||||
const domainConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = dns.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`upsert: ${name} for zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
const zone = await getZone(domainConfig, zoneName);
|
||||
const records = await getRecords(domainConfig, zone, name, type);
|
||||
|
||||
// update means first delete then recreate
|
||||
if (records.length) await del(domainObject, location, type, values);
|
||||
|
||||
const data = {
|
||||
name,
|
||||
type,
|
||||
ttl: 60,
|
||||
records: values.map(v => { return { value: v, comment: 'managed by cloudron' }; }),
|
||||
labels: {
|
||||
managedBy: 'cloudron'
|
||||
}
|
||||
};
|
||||
|
||||
const [error, response] = await safe(superagent.post(`${ENDPOINT}/zones/${zone.id}/rrsets`)
|
||||
.set('Authorization', `Bearer ${domainConfig.token}`)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status === 422) throw new BoxError(BoxError.BAD_FIELD, response.body.message);
|
||||
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
|
||||
await waitForAction(domainConfig, response.body.action.id);
|
||||
}
|
||||
|
||||
async function get(domainObject, location, type) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
|
||||
const domainConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = dns.getName(domainObject, location, type) || '@';
|
||||
|
||||
const zone = await getZone(domainConfig, zoneName);
|
||||
const result = await getRecords(domainConfig, zone, name, type);
|
||||
|
||||
return result.map(function (record) { return record.value; });
|
||||
}
|
||||
|
||||
async function del(domainObject, location, type, values) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
|
||||
const domainConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = dns.getName(domainObject, location, type) || '@';
|
||||
|
||||
const zone = await getZone(domainConfig, zoneName);
|
||||
const records = await getRecords(domainConfig, zone, name, type);
|
||||
if (records.length === 0) return;
|
||||
|
||||
const [error, response] = await safe(superagent.del(`${ENDPOINT}/zones/${zone.id}/rrsets/${name}/${type}`)
|
||||
.set('Authorization', `Bearer ${domainConfig.token}`)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 404) return;
|
||||
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
|
||||
await waitForAction(domainConfig, response.body.action.id);
|
||||
}
|
||||
|
||||
async function wait(domainObject, subdomain, type, value, options) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
|
||||
const fqdn = dns.fqdn(subdomain, domainObject.domain);
|
||||
|
||||
await waitForDns(fqdn, domainObject.zoneName, type, value, options);
|
||||
}
|
||||
|
||||
async function verifyDomainConfig(domainObject) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
|
||||
const domainConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName;
|
||||
|
||||
if (!domainConfig.token || typeof domainConfig.token !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string');
|
||||
if ('customNameservers' in domainConfig && typeof domainConfig.customNameservers !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'customNameservers must be a boolean');
|
||||
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
const credentials = {
|
||||
token: domainConfig.token,
|
||||
customNameservers: !!domainConfig.customNameservers
|
||||
};
|
||||
|
||||
if (constants.TEST) return credentials; // this shouldn't be here
|
||||
|
||||
const [error, nameservers] = await safe(dig.resolve(zoneName, 'NS', { timeout: 5000 }));
|
||||
if (error && error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain');
|
||||
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.every(function (n) { return n.toLowerCase().search(/hetzner|your-server|second-ns/) !== -1; })) {
|
||||
debug('verifyDomainConfig: %j does not contain Hetzner NS', nameservers);
|
||||
if (!domainConfig.customNameservers) throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Hetzner');
|
||||
}
|
||||
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
await upsert(domainObject, location, 'A', [ ip ]);
|
||||
debug('verifyDomainConfig: Test A record added');
|
||||
|
||||
await del(domainObject, location, 'A', [ ip ]);
|
||||
debug('verifyDomainConfig: Test A record removed again');
|
||||
|
||||
return credentials;
|
||||
}
|
||||
@@ -65,6 +65,7 @@ function api(provider) {
|
||||
case 'gandi': return require('./dns/gandi.js');
|
||||
case 'godaddy': return require('./dns/godaddy.js');
|
||||
case 'hetzner': return require('./dns/hetzner.js');
|
||||
case 'hetznercloud': return require('./dns/hetznercloud.js');
|
||||
case 'inwx': return require('./dns/inwx.js');
|
||||
case 'linode': return require('./dns/linode.js');
|
||||
case 'vultr': return require('./dns/vultr.js');
|
||||
|
||||
+17
-11
@@ -463,27 +463,27 @@ async function syncGroupMembers(config, progressCallback) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allGroups = await groups.list();
|
||||
const allGroups = await groups.listWithMembers();
|
||||
const ldapGroups = allGroups.filter(function (g) { return g.source === 'ldap'; });
|
||||
debug(`syncGroupMembers: Found ${ldapGroups.length} groups to sync users`);
|
||||
|
||||
for (const group of ldapGroups) {
|
||||
debug(`syncGroupMembers: Sync users for group ${group.name}`);
|
||||
for (const ldapGroup of ldapGroups) {
|
||||
debug(`syncGroupMembers: Sync users for group ${ldapGroup.name}`);
|
||||
|
||||
const result = await ldapGroupSearch(config, {});
|
||||
if (!result || result.length === 0) {
|
||||
debug(`syncGroupMembers: Unable to find group ${group.name} ignoring for now.`);
|
||||
debug(`syncGroupMembers: Unable to find group ${ldapGroup.name} ignoring for now.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// since our group names are lowercase we cannot use potentially case matching ldap filters
|
||||
const found = result.find(function (r) {
|
||||
if (!r[config.groupnameField]) return false;
|
||||
return r[config.groupnameField].toLowerCase() === group.name;
|
||||
return r[config.groupnameField].toLowerCase() === ldapGroup.name;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
debug(`syncGroupMembers: Unable to find group ${group.name} ignoring for now.`);
|
||||
debug(`syncGroupMembers: Unable to find group ${ldapGroup.name} ignoring for now.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -492,17 +492,17 @@ async function syncGroupMembers(config, progressCallback) {
|
||||
// if only one entry is in the group ldap returns a string, not an array!
|
||||
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
|
||||
|
||||
debug(`syncGroupMembers: Group ${group.name} has ${ldapGroupMembers.length} members.`);
|
||||
debug(`syncGroupMembers: Group ${ldapGroup.name} has ${ldapGroupMembers.length} members.`);
|
||||
|
||||
const userIds = [];
|
||||
for (const memberDn of ldapGroupMembers) {
|
||||
const [ldapError, result] = await safe(ldapGetByDN(config, memberDn));
|
||||
if (ldapError) {
|
||||
debug(`syncGroupMembers: Group ${group.name} failed to get ${memberDn}: %o`, ldapError);
|
||||
debug(`syncGroupMembers: Group ${ldapGroup.name} failed to get ${memberDn}: %o`, ldapError);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug(`syncGroupMembers: Group ${group.name} has member object ${memberDn}`);
|
||||
debug(`syncGroupMembers: Group ${ldapGroup.name} has member object ${memberDn}`);
|
||||
|
||||
const username = result[config.usernameField]?.toLowerCase();
|
||||
if (!username) continue;
|
||||
@@ -515,8 +515,14 @@ async function syncGroupMembers(config, progressCallback) {
|
||||
|
||||
userIds.push(userObject.id);
|
||||
}
|
||||
const [setError] = await safe(groups.setMembers(group, userIds, { skipSourceCheck: true }, AuditSource.EXTERNAL_LDAP));
|
||||
if (setError) debug(`syncGroupMembers: Failed to set members of group ${group.name}. %o`, setError);
|
||||
const membersChanged = ldapGroup.userIds.length !== userIds.length || ldapGroup.userIds.some(id => !userIds.includes(id));
|
||||
if (membersChanged) {
|
||||
debug(`syncGroupMembers: Group ${ldapGroup.name} changed.`);
|
||||
const [setError] = await safe(groups.setMembers(ldapGroup, userIds, { skipSourceCheck: true }, AuditSource.EXTERNAL_LDAP));
|
||||
if (setError) debug(`syncGroupMembers: Failed to set members of group ${ldapGroup.name}. %o`, setError);
|
||||
} else {
|
||||
debug(`syncGroupMembers: Group ${ldapGroup.name} is unchanged.`);
|
||||
}
|
||||
}
|
||||
|
||||
debug('syncGroupMembers: done');
|
||||
|
||||
+8
-3
@@ -90,13 +90,18 @@ async function del(group, auditSource) {
|
||||
assert.strictEqual(typeof group, 'object');
|
||||
assert(auditSource && typeof auditSource === 'object');
|
||||
|
||||
const arSearch = `JSON_SEARCH(accessRestrictionJson, 'one', ?, NULL, '$.groups')`;
|
||||
const opSearch = `JSON_SEARCH(operatorsJson, 'one', ?, NULL, '$.groups')`;
|
||||
|
||||
const queries = [
|
||||
{ query: `UPDATE apps SET accessRestrictionJson=JSON_REMOVE(accessRestrictionJson, REPLACE(${arSearch}, '"', '')) WHERE ${arSearch} IS NOT NULL`, args: [ group.id, group.id ] },
|
||||
{ query: `UPDATE apps SET operatorsJson=JSON_REMOVE(operatorsJson, REPLACE(${opSearch}, '"', '')) WHERE ${opSearch} IS NOT NULL`, args: [ group.id, group.id ] },
|
||||
{ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ group.id ] },
|
||||
{ query: 'DELETE FROM userGroups WHERE id = ?', args: [ group.id ] }
|
||||
{ query: 'DELETE FROM userGroups WHERE id = ?', args: [ group.id ] }, // keep this the last query as we check affectedRows below
|
||||
];
|
||||
|
||||
const result = await database.transaction(queries);
|
||||
if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
|
||||
if (result[queries.length-1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
|
||||
|
||||
await eventlog.add(eventlog.ACTION_GROUP_REMOVE, auditSource, { group });
|
||||
}
|
||||
@@ -163,7 +168,7 @@ async function listWithMembers() {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' GROUP BY userGroups.id ORDER BY name');
|
||||
|
||||
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
|
||||
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : []; });
|
||||
|
||||
for (const r of results) {
|
||||
await postProcess(r);
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ server {
|
||||
|
||||
<% if ( endpoint === 'dashboard' || endpoint === 'ip' || endpoint === 'setup' ) { -%>
|
||||
# CSP headers for the dashboard resources
|
||||
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; media-src *; img-src * blob: data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; media-src *; img-src * blob: data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: data: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
<% } else { %>
|
||||
<% if (cspQuoted) { %>
|
||||
add_header Content-Security-Policy <%- cspQuoted %>;
|
||||
|
||||
@@ -22,8 +22,8 @@ class ProgressStream extends TransformStream {
|
||||
}
|
||||
|
||||
stats() {
|
||||
const totalMsecs = Date.now() - this.#startTime;
|
||||
return { startTime: this.#startTime, totalMsecs, transferred: this.#transferred };
|
||||
const duration = Date.now() - this.#startTime;
|
||||
return { startTime: this.#startTime, duration, transferred: this.#transferred };
|
||||
}
|
||||
|
||||
_start() {
|
||||
|
||||
@@ -266,7 +266,7 @@ async function listBackups(req, res, next) {
|
||||
const perPage = typeof req.query.per_page === 'string'? parseInt(req.query.per_page) : 25;
|
||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||
|
||||
const [error, results] = await safe(backups.listBySiteId(req.resources.backupSite.id, page, perPage));
|
||||
const [error, results] = await safe(backups.listByTypePaged(backups.BACKUP_TYPE_BOX, req.resources.backupSite.id, page, perPage));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
next(new HttpSuccess(200, { backups: results }));
|
||||
}
|
||||
|
||||
+5
-5
@@ -114,9 +114,9 @@ async function getFilesystems() {
|
||||
}
|
||||
|
||||
const standardPaths = [
|
||||
{ type: 'standard', id: 'platformdata', name: 'Platform Data', path: paths.PLATFORM_DATA_DIR },
|
||||
{ type: 'standard', id: 'boxdata', name: 'Box Data', path: paths.BOX_DATA_DIR },
|
||||
{ type: 'standard', id: 'maildata', name: 'Mail Data', path: paths.MAIL_DATA_DIR },
|
||||
{ type: 'standard', id: 'platformdata', name: 'Platform data', path: paths.PLATFORM_DATA_DIR },
|
||||
{ type: 'standard', id: 'boxdata', name: 'Box data', path: paths.BOX_DATA_DIR },
|
||||
{ type: 'standard', id: 'maildata', name: 'Mail data', path: paths.MAIL_DATA_DIR },
|
||||
];
|
||||
|
||||
for (const stdPath of standardPaths) {
|
||||
@@ -140,7 +140,7 @@ async function getFilesystems() {
|
||||
if (!siteForDefault) {
|
||||
const [, dfResult] = await safe(df.file(paths.DEFAULT_BACKUP_DIR));
|
||||
const filesystem = dfResult?.filesystem || rootDisk.filesystem;
|
||||
if (filesystems[filesystem]) filesystems[filesystem].contents.push({ type: 'cloudron-backup-default', id: 'cloudron-backup-default', name: 'Cloudron Default Backup', path: paths.DEFAULT_BACKUP_DIR });
|
||||
if (filesystems[filesystem]) filesystems[filesystem].contents.push({ type: 'cloudron-backup-default', id: 'cloudron-backup-default', name: 'Default backup', path: paths.DEFAULT_BACKUP_DIR });
|
||||
}
|
||||
|
||||
const [dockerError, dockerInfo] = await safe(docker.info());
|
||||
@@ -149,7 +149,7 @@ async function getFilesystems() {
|
||||
const filesystem = dfResult?.filesystem || rootDisk.filesystem;
|
||||
if (filesystems[filesystem]) {
|
||||
filesystems[filesystem].contents.push({ type: 'standard', id: 'docker', name: 'Docker', path: dockerInfo.DockerRootDir });
|
||||
filesystems[filesystem].contents.push({ type: 'standard', id: 'docker-volumes', name: 'Docker Volumes', path: dockerInfo.DockerRootDir });
|
||||
filesystems[filesystem].contents.push({ type: 'standard', id: 'docker-volumes', name: 'Docker volumes', path: dockerInfo.DockerRootDir });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ describe('backup cleaner', function () {
|
||||
it('succeeds with box backups, keeps latest', async function () {
|
||||
await cleanupBackups(site);
|
||||
|
||||
const results = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, 1, 1000);
|
||||
const results = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, site.id, 1, 1000);
|
||||
expect(results.length).to.equal(1);
|
||||
expect(results[0].id).to.equal(BACKUP_1_BOX.id);
|
||||
|
||||
@@ -300,7 +300,7 @@ describe('backup cleaner', function () {
|
||||
it('does not remove expired backups if only one left', async function () {
|
||||
await cleanupBackups(site);
|
||||
|
||||
const results = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, 1, 1000);
|
||||
const results = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, site.id, 1, 1000);
|
||||
expect(results[0].id).to.equal(BACKUP_1_BOX.id);
|
||||
|
||||
// check that app backups are also still there. backup_1 is still there
|
||||
@@ -318,7 +318,7 @@ describe('backup cleaner', function () {
|
||||
|
||||
await cleanupBackups(site);
|
||||
|
||||
let result = await backups.listByTypePaged(backups.BACKUP_TYPE_APP, 1, 1000);
|
||||
let result = await backups.listByTypePaged(backups.BACKUP_TYPE_APP, site.id, 1, 1000);
|
||||
expect(result.length).to.equal(4);
|
||||
result = result.sort((r1, r2) => r1.remotePath.localeCompare(r2.remotePath));
|
||||
expect(result[0].id).to.be(BACKUP_0_APP_0.id); // because app is installed, latest backup is preserved
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('backups', function () {
|
||||
});
|
||||
|
||||
it('listByTypePaged succeeds', async function () {
|
||||
const results = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, 1, 5);
|
||||
const results = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, defaultBackupSite.id, 1, 5);
|
||||
expect(results.length).to.be(1);
|
||||
delete results[0].creationTime;
|
||||
expect(results[0]).to.eql(boxBackup);
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('backups', function () {
|
||||
backupSite.integrityKeyPair = {}; // keep removePrivateFields happy
|
||||
await backupSites.del(backupSite, auditSource);
|
||||
|
||||
expect(await backups.list(1, 10)).to.eql([]);
|
||||
expect(await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, defaultBackupSite.id, 1, 10)).to.eql([]);
|
||||
expect(await archives.list(1, 10)).to.eql([]);
|
||||
expect(await backupSites.list()).to.eql([]);
|
||||
});
|
||||
|
||||
+12
-6
@@ -316,16 +316,22 @@ async function del(user, auditSource) {
|
||||
|
||||
if (constants.DEMO && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
|
||||
|
||||
const queries = [];
|
||||
queries.push({ query: 'DELETE FROM groupMembers WHERE userId = ?', args: [ user.id ] });
|
||||
queries.push({ query: 'DELETE FROM tokens WHERE identifier = ?', args: [ user.id ] });
|
||||
queries.push({ query: 'DELETE FROM appPasswords WHERE userId = ?', args: [ user.id ] });
|
||||
queries.push({ query: 'DELETE FROM users WHERE id = ?', args: [ user.id ] });
|
||||
const arSearch = `JSON_SEARCH(accessRestrictionJson, 'one', ?, NULL, '$.users')`;
|
||||
const opSearch = `JSON_SEARCH(operatorsJson, 'one', ?, NULL, '$.users')`;
|
||||
|
||||
const queries = [
|
||||
{ query: `UPDATE apps SET accessRestrictionJson=JSON_REMOVE(accessRestrictionJson, REPLACE(${arSearch}, '"', '')) WHERE ${arSearch} IS NOT NULL`, args: [ user.id, user.id ] },
|
||||
{ query: `UPDATE apps SET operatorsJson=JSON_REMOVE(operatorsJson, REPLACE(${opSearch}, '"', '')) WHERE ${opSearch} IS NOT NULL`, args: [ user.id, user.id ] },
|
||||
{ query: 'DELETE FROM groupMembers WHERE userId = ?', args: [ user.id ] },
|
||||
{ query: 'DELETE FROM tokens WHERE identifier = ?', args: [ user.id ] },
|
||||
{ query: 'DELETE FROM appPasswords WHERE userId = ?', args: [ user.id ] },
|
||||
{ query: 'DELETE FROM users WHERE id = ?', args: [ user.id ] }, // keep this the last query as we check affectedRows below
|
||||
];
|
||||
|
||||
const [error, result] = await safe(database.transaction(queries));
|
||||
if (error && error.sqlCode === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, error);
|
||||
if (error) throw error;
|
||||
if (result[3].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
if (result[queries.length-1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
|
||||
await eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: user.id, user: removePrivateFields(user) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user