Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2926871eab | |||
| 5b05ea285c | |||
| 48a2e6881f | |||
| edbeaa2f77 | |||
| 48a85a620d | |||
| cc8db71ecf | |||
| e4573f74a4 | |||
| 8cff72cf59 | |||
| 73a9de7708 | |||
| 104318ab8c | |||
| 8ec4659949 | |||
| ffa8ff8427 | |||
| 4ef1339ba2 | |||
| 3702efdcb3 | |||
| bbdfbe1ab7 | |||
| cc1fc5c269 | |||
| bc32fa64bf | |||
| cfc7de9c77 | |||
| 945ab30373 | |||
| 494125227f | |||
| a4919b06f9 | |||
| 790ba406bf | |||
| e0367056bd | |||
| 4bf0dc192c | |||
| 4575a0ddce | |||
| 837cbff092 | |||
| 4108047644 | |||
| 347cf4f67d | |||
| 7f9344a556 | |||
| 8907b692c1 | |||
| 6c0d5cb601 | |||
| 5c69a146f6 | |||
| de75ae5b9e | |||
| 9c9e2c6a62 | |||
| 917c18a423 | |||
| aac81c2fba | |||
| 9e82839fb7 | |||
| ae2f74777b | |||
| 4c5d67606f | |||
| 0d2a0f91c7 | |||
| b65fa3e2c7 | |||
| e87d2e1218 | |||
| 00ae320b51 | |||
| 3d46d24038 | |||
| 8b04484ff7 | |||
| 7f9f3f683b | |||
| fb2ce06621 | |||
| 89f5e87601 | |||
| e124755363 | |||
| d0ccbe2786 | |||
| 25dec602b8 | |||
| bbf7007250 | |||
| 2b4f8ff00d | |||
| b467b58ee7 | |||
| facefeddae | |||
| 141bdb1307 | |||
| b53da61e7c | |||
| ede93323af | |||
| 8ccf79175a | |||
| 9fa330a0a0 |
@@ -3067,3 +3067,20 @@
|
||||
* mailinglist: fix search on name
|
||||
* backup site: fix migration with mixed formats
|
||||
|
||||
[9.0.12]
|
||||
* eventlog: always fetch enough event logs to fill the screen
|
||||
* mail: check for outbound ipv6 connectivity
|
||||
* store actual appId not oidc clientId for log in events
|
||||
* Add english labels for eventlog filtering
|
||||
* mail: when deferred, show reason
|
||||
* mail: prefer ipv4 for outbound mail
|
||||
|
||||
[9.0.13]
|
||||
* Fix issue where footer/name can break templates
|
||||
* rsync: bump empty dir limit to 80k
|
||||
* nginx: do not log query params
|
||||
* Fetch mailbox usage in the background to not delay mailbox listing
|
||||
* cloudron-support: add --check-services and add it to troubleshoot
|
||||
* Do not poll services if they are in recoveryMode
|
||||
* restore/import: fix issue where prefix was empty
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
<title><%= name %> OpenID Error</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.errorMessage = `<%- errorMessage %>`;
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
window.cloudron = <%- JSON.stringify({
|
||||
iconUrl: iconUrl,
|
||||
name: name,
|
||||
errorMessage: errorMessage,
|
||||
footer: footer,
|
||||
language: language
|
||||
}) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
<title><%= name %> OpenID Access Denied</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.submitUrl = '<%- submitUrl %>';
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
window.cloudron = <%- JSON.stringify({
|
||||
iconUrl: iconUrl,
|
||||
name: name,
|
||||
submitUrl: submitUrl,
|
||||
footer: footer,
|
||||
language: language
|
||||
}) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
<title><%= name %> Login</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.note = '<%- note %>';
|
||||
window.cloudron.submitUrl = '<%- submitUrl %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
window.cloudron = <%- JSON.stringify({
|
||||
iconUrl: iconUrl,
|
||||
name: name,
|
||||
note: note,
|
||||
submitUrl: submitUrl,
|
||||
footer: footer,
|
||||
language: language
|
||||
}) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
Generated
+246
-224
@@ -6,26 +6,26 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@cloudron/pankow": "^3.5.9",
|
||||
"@cloudron/pankow": "^3.5.11",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"anser": "^2.3.2",
|
||||
"anser": "^2.3.3",
|
||||
"async": "^3.2.6",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"marked": "^17.0.0",
|
||||
"eslint-plugin-vue": "^10.6.1",
|
||||
"marked": "^17.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"vite": "^7.2.2",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.6.3"
|
||||
}
|
||||
},
|
||||
@@ -76,15 +76,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudron/pankow": {
|
||||
"version": "3.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.9.tgz",
|
||||
"integrity": "sha512-59aAGwAdOGwSi3csh+jf6+cOEB5IyJrgppooyfj8K031Go145CmN3rD7J1eeVhZRDBa9zjbHNTSqs/rMqkwyEA==",
|
||||
"version": "3.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.11.tgz",
|
||||
"integrity": "sha512-KH1is+ZPfUyv2OkckpAFl+jEIYBf/9+qbljtIByAbTeOkjyGEG79W2Zv1k/YkcS9IoYR8oib5rGtzLo220igYg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"filesize": "^11.0.13",
|
||||
"monaco-editor": "^0.54.0"
|
||||
"monaco-editor": "^0.55.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
@@ -699,13 +699,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
|
||||
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.2.tgz",
|
||||
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.1.12",
|
||||
"@intlify/shared": "11.1.12"
|
||||
"@intlify/message-compiler": "11.2.2",
|
||||
"@intlify/shared": "11.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -715,12 +715,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
|
||||
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.2.tgz",
|
||||
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.1.12",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -731,9 +731,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
|
||||
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.2.tgz",
|
||||
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -754,9 +754,9 @@
|
||||
"integrity": "sha512-hW0GwZj06z/ZFUW2Espl7toVDjghJN+EKqyXzPSV8NV89d5BYp5rRMBJoc+aUN0x5OXDMeRQHazejr2Xmqj2tw=="
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.29",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
|
||||
"integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==",
|
||||
"version": "1.0.0-beta.50",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",
|
||||
"integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
@@ -1031,13 +1031,20 @@
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
"integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-beta.29"
|
||||
"@rolldown/pluginutils": "1.0.0-beta.50"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -1048,39 +1055,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz",
|
||||
"integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.24",
|
||||
"@vue/shared": "3.5.25",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz",
|
||||
"integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz",
|
||||
"integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
|
||||
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.24",
|
||||
"@vue/compiler-dom": "3.5.24",
|
||||
"@vue/compiler-ssr": "3.5.24",
|
||||
"@vue/shared": "3.5.24",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -1088,13 +1095,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz",
|
||||
"integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
|
||||
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -1103,53 +1110,53 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
|
||||
"integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz",
|
||||
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
|
||||
"integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
|
||||
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
|
||||
"integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.24",
|
||||
"@vue/runtime-core": "3.5.24",
|
||||
"@vue/shared": "3.5.24",
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
|
||||
"integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
|
||||
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.24"
|
||||
"vue": "3.5.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz",
|
||||
"integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz",
|
||||
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/addon-attach": {
|
||||
@@ -1211,9 +1218,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/anser": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.2.tgz",
|
||||
"integrity": "sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw==",
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.3.tgz",
|
||||
"integrity": "sha512-QGY1oxYE7/kkeNmbtY/2ZjQ07BCG3zYdz+k/+sf69kMzEIxb93guHkPnIXITQ+BYi61oQwG74twMOX1tD4aesg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
@@ -1363,6 +1370,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
},
|
||||
@@ -1371,9 +1379,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@@ -1399,10 +1407,13 @@
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
@@ -1516,15 +1527,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue": {
|
||||
"version": "10.5.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.5.1.tgz",
|
||||
"integrity": "sha512-SbR9ZBUFKgvWAbq3RrdCtWaW0IKm6wwUiApxf3BVTNfqUIo4IQQmreMg2iHFJJ6C/0wss3LXURBJ1OwS/MhFcQ==",
|
||||
"version": "10.6.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.6.1.tgz",
|
||||
"integrity": "sha512-OMvDAFbewocYrJamF1EoSWoT4xa7/QRb/yYouEZMiroTE+WRmFUreR+kAFQHqM45W3kg5oljVfUYfH9HEwX1Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"nth-check": "^2.1.1",
|
||||
"postcss-selector-parser": "^6.0.15",
|
||||
"postcss-selector-parser": "^7.1.0",
|
||||
"semver": "^7.6.3",
|
||||
"xml-name-validator": "^4.0.0"
|
||||
},
|
||||
@@ -1926,9 +1937,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz",
|
||||
"integrity": "sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==",
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
@@ -1983,12 +1994,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.54.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz",
|
||||
"integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==",
|
||||
"version": "0.55.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dompurify": "3.1.7",
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
@@ -2160,9 +2171,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -2380,12 +2392,13 @@
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -2502,16 +2515,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz",
|
||||
"integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.24",
|
||||
"@vue/compiler-sfc": "3.5.24",
|
||||
"@vue/runtime-dom": "3.5.24",
|
||||
"@vue/server-renderer": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
"@vue/runtime-dom": "3.5.25",
|
||||
"@vue/server-renderer": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -2548,13 +2561,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
|
||||
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.2.tgz",
|
||||
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.1.12",
|
||||
"@intlify/shared": "11.1.12",
|
||||
"@intlify/core-base": "11.2.2",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2654,14 +2667,14 @@
|
||||
}
|
||||
},
|
||||
"@cloudron/pankow": {
|
||||
"version": "3.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.9.tgz",
|
||||
"integrity": "sha512-59aAGwAdOGwSi3csh+jf6+cOEB5IyJrgppooyfj8K031Go145CmN3rD7J1eeVhZRDBa9zjbHNTSqs/rMqkwyEA==",
|
||||
"version": "3.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-3.5.11.tgz",
|
||||
"integrity": "sha512-KH1is+ZPfUyv2OkckpAFl+jEIYBf/9+qbljtIByAbTeOkjyGEG79W2Zv1k/YkcS9IoYR8oib5rGtzLo220igYg==",
|
||||
"requires": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"filesize": "^11.0.13",
|
||||
"monaco-editor": "^0.54.0"
|
||||
"monaco-editor": "^0.55.1"
|
||||
}
|
||||
},
|
||||
"@esbuild/aix-ppc64": {
|
||||
@@ -2937,27 +2950,27 @@
|
||||
"integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="
|
||||
},
|
||||
"@intlify/core-base": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
|
||||
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.2.tgz",
|
||||
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==",
|
||||
"requires": {
|
||||
"@intlify/message-compiler": "11.1.12",
|
||||
"@intlify/shared": "11.1.12"
|
||||
"@intlify/message-compiler": "11.2.2",
|
||||
"@intlify/shared": "11.2.2"
|
||||
}
|
||||
},
|
||||
"@intlify/message-compiler": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
|
||||
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.2.tgz",
|
||||
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==",
|
||||
"requires": {
|
||||
"@intlify/shared": "11.1.12",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"@intlify/shared": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
|
||||
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A=="
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.2.tgz",
|
||||
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw=="
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
@@ -2970,9 +2983,9 @@
|
||||
"integrity": "sha512-hW0GwZj06z/ZFUW2Espl7toVDjghJN+EKqyXzPSV8NV89d5BYp5rRMBJoc+aUN0x5OXDMeRQHazejr2Xmqj2tw=="
|
||||
},
|
||||
"@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.29",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
|
||||
"integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q=="
|
||||
"version": "1.0.0-beta.50",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",
|
||||
"integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="
|
||||
},
|
||||
"@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.45.1",
|
||||
@@ -3104,45 +3117,51 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||
},
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
"integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==",
|
||||
"requires": {
|
||||
"@rolldown/pluginutils": "1.0.0-beta.29"
|
||||
"@rolldown/pluginutils": "1.0.0-beta.50"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz",
|
||||
"integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.24",
|
||||
"@vue/shared": "3.5.25",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz",
|
||||
"integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz",
|
||||
"integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
|
||||
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.24",
|
||||
"@vue/compiler-dom": "3.5.24",
|
||||
"@vue/compiler-ssr": "3.5.24",
|
||||
"@vue/shared": "3.5.24",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -3150,12 +3169,12 @@
|
||||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz",
|
||||
"integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
|
||||
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"@vue/devtools-api": {
|
||||
@@ -3164,46 +3183,46 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
|
||||
"integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz",
|
||||
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
|
||||
"requires": {
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-core": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
|
||||
"integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
|
||||
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-dom": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
|
||||
"integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.24",
|
||||
"@vue/runtime-core": "3.5.24",
|
||||
"@vue/shared": "3.5.24",
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"@vue/server-renderer": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
|
||||
"integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
|
||||
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
|
||||
"requires": {
|
||||
"@vue/compiler-ssr": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz",
|
||||
"integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz",
|
||||
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg=="
|
||||
},
|
||||
"@xterm/addon-attach": {
|
||||
"version": "0.11.0",
|
||||
@@ -3245,9 +3264,9 @@
|
||||
}
|
||||
},
|
||||
"anser": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.2.tgz",
|
||||
"integrity": "sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw=="
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.3.tgz",
|
||||
"integrity": "sha512-QGY1oxYE7/kkeNmbtY/2ZjQ07BCG3zYdz+k/+sf69kMzEIxb93guHkPnIXITQ+BYi61oQwG74twMOX1tD4aesg=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
@@ -3356,9 +3375,9 @@
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
|
||||
},
|
||||
"csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.4.0",
|
||||
@@ -3374,9 +3393,12 @@
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ=="
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"requires": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "4.5.0",
|
||||
@@ -3486,14 +3508,14 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
"version": "10.5.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.5.1.tgz",
|
||||
"integrity": "sha512-SbR9ZBUFKgvWAbq3RrdCtWaW0IKm6wwUiApxf3BVTNfqUIo4IQQmreMg2iHFJJ6C/0wss3LXURBJ1OwS/MhFcQ==",
|
||||
"version": "10.6.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.6.1.tgz",
|
||||
"integrity": "sha512-OMvDAFbewocYrJamF1EoSWoT4xa7/QRb/yYouEZMiroTE+WRmFUreR+kAFQHqM45W3kg5oljVfUYfH9HEwX1Bg==",
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"nth-check": "^2.1.1",
|
||||
"postcss-selector-parser": "^6.0.15",
|
||||
"postcss-selector-parser": "^7.1.0",
|
||||
"semver": "^7.6.3",
|
||||
"xml-name-validator": "^4.0.0"
|
||||
}
|
||||
@@ -3724,9 +3746,9 @@
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz",
|
||||
"integrity": "sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg=="
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.8",
|
||||
@@ -3759,11 +3781,11 @@
|
||||
}
|
||||
},
|
||||
"monaco-editor": {
|
||||
"version": "0.54.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz",
|
||||
"integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==",
|
||||
"version": "0.55.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"requires": {
|
||||
"dompurify": "3.1.7",
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -3867,9 +3889,9 @@
|
||||
}
|
||||
},
|
||||
"postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"requires": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -4004,12 +4026,12 @@
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"vite": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"requires": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4042,15 +4064,15 @@
|
||||
}
|
||||
},
|
||||
"vue": {
|
||||
"version": "3.5.24",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz",
|
||||
"integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==",
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.24",
|
||||
"@vue/compiler-sfc": "3.5.24",
|
||||
"@vue/runtime-dom": "3.5.24",
|
||||
"@vue/server-renderer": "3.5.24",
|
||||
"@vue/shared": "3.5.24"
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
"@vue/runtime-dom": "3.5.25",
|
||||
"@vue/server-renderer": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
@@ -4069,12 +4091,12 @@
|
||||
}
|
||||
},
|
||||
"vue-i18n": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
|
||||
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.2.tgz",
|
||||
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==",
|
||||
"requires": {
|
||||
"@intlify/core-base": "11.1.12",
|
||||
"@intlify/shared": "11.1.12",
|
||||
"@intlify/core-base": "11.2.2",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,26 +7,26 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@cloudron/pankow": "^3.5.9",
|
||||
"@cloudron/pankow": "^3.5.11",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"anser": "^2.3.2",
|
||||
"anser": "^2.3.3",
|
||||
"async": "^3.2.6",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"marked": "^17.0.0",
|
||||
"eslint-plugin-vue": "^10.6.1",
|
||||
"marked": "^17.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"vite": "^7.2.2",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<title><%= name %> Password Reset</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
window.cloudron = <%- JSON.stringify({
|
||||
name: name,
|
||||
footer: footer,
|
||||
language: language
|
||||
}) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
<title><%= name %> Login</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.loginUrl = '<%- loginUrl %>';
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
window.cloudron.apiOrigin = `<%= apiOrigin %>`;
|
||||
window.cloudron = <%- JSON.stringify({
|
||||
name: name,
|
||||
iconUrl: iconUrl,
|
||||
loginUrl: loginUrl,
|
||||
language: language,
|
||||
apiOrigin: apiOrigin
|
||||
}) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -986,9 +986,9 @@
|
||||
"disableAction": "Deaktvieren"
|
||||
},
|
||||
"deleteMailboxDialog": {
|
||||
"description": "Nach dem Löschen werden E-Mails an dieses Postfach zurückgeschickt. E-Mails in diesem Postfach nicht löschen, wenn sie archiviert werden sollen. Die zu archivierenden E-Mails befinden sich unter <code>/home/yellowtent/boxdata/mail/vmail</code> auf dem Server.",
|
||||
"description": "Nach dem Löschen werden E-Mails an dieses Postfach zurückgeschickt. E-Mails in diesem Postfach nicht löschen, wenn sie archiviert werden sollen. Die zu archivierenden E-Mails befinden sich unter <code>/home/yellowtent/boxdata/mail/vmail</code> auf dem Server.<br/><br/>Postfach \"{{ name }}@{{ domain }}\" löschen?",
|
||||
"deleteAction": "Löschen",
|
||||
"title": "Postfach {{ name }}@{{ domain }} löschen",
|
||||
"title": "Postfach löschen",
|
||||
"purgeMailboxCheckbox": "Alle E-Mails und Filter dieses Postfaches löschen"
|
||||
},
|
||||
"editMailinglistDialog": {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"main": {
|
||||
"offline": "Cloudron is offline. Reconnecting…",
|
||||
"logout": "Logout",
|
||||
"logout": "Log out",
|
||||
"dialog": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
@@ -162,12 +162,12 @@
|
||||
"role": "Role",
|
||||
"groups": "Groups",
|
||||
"noGroups": "No groups available.",
|
||||
"usernamePlaceholder": "Optional. If not provided, user can pick during sign up",
|
||||
"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",
|
||||
"displayNamePlaceholder": "Optional. If not provided, user can provide during sign up",
|
||||
"displayNamePlaceholder": "Optional. If not provided, user can provide during sign up.",
|
||||
"fallbackEmailPlaceholder": "If not specified, primary email will be used"
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
@@ -300,7 +300,7 @@
|
||||
"loginTokens": {
|
||||
"title": "Login Tokens",
|
||||
"description": "You have {{ webadminTokenCount}} active web token(s) and {{ cliTokenCount }} CLI token(s).",
|
||||
"logoutAll": "Logout from all"
|
||||
"logoutAll": "Log out from all"
|
||||
},
|
||||
"changeEmail": {
|
||||
"title": "Change Primary Email",
|
||||
@@ -560,8 +560,8 @@
|
||||
"customRulesPlaceholder": "Custom Spamassassin Rules"
|
||||
},
|
||||
"testMailDialog": {
|
||||
"title": "Send test email for {{ domain }}",
|
||||
"description": "This will send a test email from <b>no-reply@{{ domain }}</b> to the address below.",
|
||||
"title": "Send test email",
|
||||
"description": "Sends a test email from <b>no-reply@{{ domain }}</b> to the specified address.",
|
||||
"sendAction": "Send"
|
||||
},
|
||||
"solrConfig": {
|
||||
@@ -788,7 +788,7 @@
|
||||
"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",
|
||||
"advancedAction": "Advanced settings…",
|
||||
"zoneName": "Zone name (optional)",
|
||||
"zoneName": "Zone name",
|
||||
"fallbackCert": "Fallback Certificate (optional)",
|
||||
"fallbackCertCustomCert": "Custom Certificate",
|
||||
"fallbackCertCustomCertInfo": "Provide a <a href=\"{{ customCertLink }}\" target=\"_blank\">wildcard certificate</a> to use for all apps on this domain. If not provided, a self-signed certificate is automatically generated.",
|
||||
@@ -818,7 +818,8 @@
|
||||
"gandiTokenTypePAT": "Personal Access Token (PAT)",
|
||||
"inwxUsername": "INWX username",
|
||||
"inwxPassword": "INWX password",
|
||||
"customNameservers": "Domain uses custom (vanity) nameservers"
|
||||
"customNameservers": "Domain uses custom (vanity) nameservers",
|
||||
"zoneNamePlaceholder": "Optional. If not provided, defaults to the root domain."
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Remove Domain",
|
||||
|
||||
@@ -166,9 +166,9 @@
|
||||
"primaryEmail": "Primair e-mailadres",
|
||||
"recoveryEmail": "Wachtwoordherstel e-mailadres",
|
||||
"activeCheckbox": "Gebruiker is actief",
|
||||
"usernamePlaceholder": "Optioneel. Indien niet ingevuld mag de gebruiker bij registratie zelf kiezen",
|
||||
"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"
|
||||
"displayNamePlaceholder": "Optioneel. Indien niet ingevoerd kan de gebruiker het kiezen tijdens eerste aanmelding."
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
"deleteAction": "Verwijder",
|
||||
@@ -560,8 +560,8 @@
|
||||
"blacklisteAddressesInfo": "Overeenkomende adressen belanden in de Spam folder van de gebruikers. '*' en '?' glob patronen worden ondersteund."
|
||||
},
|
||||
"testMailDialog": {
|
||||
"title": "Verstuur test e-mail voor {{ domain }}",
|
||||
"description": "Hiermee stuur je een test e-mail van <b>no-reply@{{ domain }}</b> aan onderstaand adres.",
|
||||
"title": "Verstuur test e-mail",
|
||||
"description": "Stuur een test e-mail van <b>no-reply@{{ domain }}</b> naar het opgegeven adres.",
|
||||
"sendAction": "Verstuur"
|
||||
},
|
||||
"solrConfig": {
|
||||
@@ -606,7 +606,7 @@
|
||||
"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)",
|
||||
"zoneName": "Zone naam",
|
||||
"fallbackCert": "Reservecertificaat (optioneel)",
|
||||
"fallbackCertCustomCert": "Aangepast certificaat",
|
||||
"fallbackCertCustomCertInfo": "Voorzie een <a href=\"{{ customCertLink }}\" target=\"_blank\">wildcard certificaat</a> voor gebruik door alle apps op dit domein. Als dit niet wordt verstrekt, wordt automatisch een zelfondertekend certificaat aangemaakt.",
|
||||
@@ -639,7 +639,8 @@
|
||||
"gandiTokenTypePAT": "Persoonlijke Toegang Token (PAT)",
|
||||
"inwxUsername": "INWX gebruikersnaam",
|
||||
"inwxPassword": "INWX wachtwoord",
|
||||
"customNameservers": "Domein maakt gebruik van aangepaste (eigen) naamservers"
|
||||
"customNameservers": "Domein maakt gebruik van aangepaste (eigen) naamservers",
|
||||
"zoneNamePlaceholder": "Optioneel. Indien niet opgegeven, wordt standaard het rootdomein gebruikt."
|
||||
},
|
||||
"title": "Domeinen",
|
||||
"domain": "Domein",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"offline": "Cloudron недоступен. Переподключение…",
|
||||
"rebootDialog": {
|
||||
"title": "Перезагрузить сервер",
|
||||
"description": "Перезагружает сервер для применения обновлений или исправления непредвиденного поведения. Все приложения и сервисы будут перезапущены автоматически.",
|
||||
"description": "Все приложения и сервисы будут перезапущены автоматически. <br/><br/>Перезагрузить сервер?",
|
||||
"rebootAction": "Перезагрузить сейчас"
|
||||
},
|
||||
"logout": "Выйти",
|
||||
@@ -49,7 +49,8 @@
|
||||
"remove": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"add": "Добавить",
|
||||
"next": "Следующий"
|
||||
"next": "Следующий",
|
||||
"configure": "Настроить"
|
||||
},
|
||||
"searchPlaceholder": "Поиск",
|
||||
"multiselect": {
|
||||
@@ -72,8 +73,8 @@
|
||||
"searchPlaceholder": "Искать альтернативы GitHub, Dropbox, Slack, Trello, …",
|
||||
"installDialog": {
|
||||
"locationPlaceholder": "Оставьте пустым, чтобы использовать основной домен",
|
||||
"userManagementNone": "Приложение использует свою систему управления пользователями. Данный параметр определяет, отображается ли это приложение на панели управления пользователя.",
|
||||
"userManagementAllUsers": "Разрешить всем пользователям этого Cloudron",
|
||||
"userManagementNone": "Приложение использует свою систему управления пользователями.",
|
||||
"userManagementAllUsers": "Разрешить всем пользователям в этом Cloudron",
|
||||
"configuredForCloudronEmail": "Это приложение настроено для использования с <a href=\"{{ emailDocsLink }}\" target=\"_blank\">адресом почты Cloudron</a>.",
|
||||
"cloudflarePortWarning": "Для получения доступа к приложению через выбранный домен необходимо отключить Cloudflire прокси",
|
||||
"lastUpdated": "Был обновлён {{ date }}",
|
||||
@@ -86,7 +87,7 @@
|
||||
"users": "Пользователи",
|
||||
"groups": "Группы",
|
||||
"manualWarning": "Вручную добавьте A (IPv4) и AAAA (IPv6) запись DNS для <b> {{ location }}</b>, указав публичный IP вашего сервера",
|
||||
"userManagementMailbox": "Все пользователи этого Cloudron с почтовым ящиком имеют доступ.",
|
||||
"userManagementMailbox": "Пользователи с <a href=\"/#/mailboxes\">почтовым ящиком</a> могут войти с помощью адреса email и пароля Cloudron.",
|
||||
"portReadOnly": "Только для чтения",
|
||||
"ephemeralPortWarning": "Использование временных портов может привести к конфликтам."
|
||||
},
|
||||
@@ -132,7 +133,7 @@
|
||||
"bindPassword": "Привязать пароль (необязательно)",
|
||||
"bindUsername": "Привязать Уникальное имя (DN)/Имя пользователя (необязательно)",
|
||||
"title": "Подключиться к удалённому каталогу",
|
||||
"noopInfo": "LDAP аутентификация не настроена.",
|
||||
"noopInfo": "Внешний каталог не настроен.",
|
||||
"provider": "Провайдер",
|
||||
"server": "URL сервера",
|
||||
"acceptSelfSignedCert": "Принимать самоподписанный сертификат",
|
||||
@@ -161,13 +162,13 @@
|
||||
"role": "Роль",
|
||||
"groups": "Группы",
|
||||
"noGroups": "Нет доступных групп.",
|
||||
"usernamePlaceholder": "Необязательно. Если не указано, пользователь может выбрать во время регистрации",
|
||||
"usernamePlaceholder": "Необязательно. Если не указано, пользователь может выбрать во время регистрации.",
|
||||
"displayName": "Отображаемое имя",
|
||||
"primaryEmail": "Основной адрес электронной почты",
|
||||
"recoveryEmail": "Электронная почта для восстановления пароля",
|
||||
"primaryEmail": "Основной email",
|
||||
"recoveryEmail": "Email для восстановления пароля",
|
||||
"activeCheckbox": "Пользователь активен",
|
||||
"fallbackEmailPlaceholder": "Если не указано, будет использоваться основной почтовый ящик",
|
||||
"displayNamePlaceholder": "Необязательно. Если не указано, пользователь может указать во время регистрации"
|
||||
"fallbackEmailPlaceholder": "Если не указано, будет использоваться основной email",
|
||||
"displayNamePlaceholder": "Необязательно. Если не указано, пользователь может указать во время регистрации."
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
"title": "Удалить пользователя",
|
||||
@@ -199,18 +200,20 @@
|
||||
"mailmanager": "Менеджер пользователей и электронной почты"
|
||||
},
|
||||
"invitationDialog": {
|
||||
"title": "Пригласить {{ username }}",
|
||||
"title": "Пригласить пользователя",
|
||||
"description": "Ссылка с приглашением отправлена на электронную почту {{ email }}:",
|
||||
"sendAction": "Отправить письмо",
|
||||
"descriptionEmail": "Отправить приглашение",
|
||||
"descriptionLink": "Скопировать ссылку с приглашением"
|
||||
"descriptionEmail": "Отправить email приглашение",
|
||||
"descriptionLink": "Ссылка-приглашение",
|
||||
"context": "Пригласить пользователя \"{{ username }}\""
|
||||
},
|
||||
"setGhostDialog": {
|
||||
"description": "Установите временный пароль для доступа к приложениям и панели управления от имени данного пользователя. Такой пароль будет действовать 6 часов.",
|
||||
"title": "Вотйти от имени {{ username }}",
|
||||
"password": "Временный Пароль",
|
||||
"title": "Вотйти от имени пользователя",
|
||||
"password": "Временный пароль",
|
||||
"setPassword": "Установить пароль",
|
||||
"generatePassword": "Сгенерировать пароль"
|
||||
"generatePassword": "Сгенерировать пароль",
|
||||
"context": "Войти от имени пользователя \"{{ username }}\""
|
||||
},
|
||||
"editUserDialog": {
|
||||
"title": "Редактировать пользователя",
|
||||
@@ -233,9 +236,9 @@
|
||||
},
|
||||
"exposedLdap": {
|
||||
"ipRestriction": {
|
||||
"description": "Ограничьте доступ к серверу каталогов только для определённого круга IP-адресов и диапазонов. Строки, начинающиеся с <code>#</code>, будут считаться комментарием.",
|
||||
"placeholder": "IP-адреса или подсети, разделённые строками",
|
||||
"label": "Ограничить доступ"
|
||||
"description": "Ограничьте доступ к серверу каталогов только для определённого круга IP-адресов и диапазонов",
|
||||
"placeholder": "IP-адреса или подсети, разделённые строками. Строки, начинающиеся с <code>#</code> будут определены, как комментарии.",
|
||||
"label": "Разрешённые IP-адреса и диапазоны"
|
||||
},
|
||||
"description": "Сервер LDAP позволяет внешним приложениям аутентифицировать пользователей с использованием Каталога пользователей Cloudron.",
|
||||
"secret": {
|
||||
@@ -244,9 +247,9 @@
|
||||
"url": "URL сервера"
|
||||
},
|
||||
"cloudflarePortWarning": "Для доступа к LDAP серверу через домен панели управления проксирование Cloudflare должно быть выключено",
|
||||
"enable": "Включить Сервер LDAP",
|
||||
"enable": "Включить сервер LDAP",
|
||||
"title": "Сервер LDAP",
|
||||
"enabled": "Включить Сервер LDAP"
|
||||
"enabled": "Включить сервер LDAP"
|
||||
},
|
||||
"title": "Пользователи"
|
||||
},
|
||||
@@ -278,7 +281,7 @@
|
||||
"noPasswordsPlaceholder": "Пароли приложений отсутствуют"
|
||||
},
|
||||
"title": "Профиль",
|
||||
"primaryEmail": "Главный адрес электронной почты",
|
||||
"primaryEmail": "Основной email",
|
||||
"passwordRecoveryEmail": "Почта для восстановления пароля",
|
||||
"language": "Язык",
|
||||
"apiTokens": {
|
||||
@@ -291,7 +294,7 @@
|
||||
"scope": "Область",
|
||||
"readonly": "Только для чтения",
|
||||
"readwrite": "Чтение и запись",
|
||||
"allowedIpRangesPlaceholder": "IP адреса или подсети, разделённые запятой",
|
||||
"allowedIpRangesPlaceholder": "IP адреса или подсети, через запятую",
|
||||
"allowedIpRanges": "Разрешённые IP адреса"
|
||||
},
|
||||
"loginTokens": {
|
||||
@@ -300,7 +303,7 @@
|
||||
"logoutAll": "Выйти из всех"
|
||||
},
|
||||
"changeEmail": {
|
||||
"title": "Изменить главный Email",
|
||||
"title": "Изменить основной Email",
|
||||
"email": "Новый Email",
|
||||
"password": "Подтверждение паролем"
|
||||
},
|
||||
@@ -317,7 +320,7 @@
|
||||
"createApiToken": {
|
||||
"copyNow": "Пожалуйста, скопируйте сгенерированный API Токен. Он не будет показан снова из соображений безопасности.",
|
||||
"title": "Добавить API Токен",
|
||||
"name": "Имя API Токена",
|
||||
"name": "Имя API токена",
|
||||
"description": "Новый API Токен:",
|
||||
"access": "API доступ",
|
||||
"allowedIpRanges": "Разрешённые диапазоны IP"
|
||||
@@ -341,7 +344,7 @@
|
||||
"uninstallDialog": {
|
||||
"uninstallAction": "Удалить",
|
||||
"title": "Удалить {{ app }}",
|
||||
"description": "Данное действие безвозвратно удалит {{ app }} и все его данные."
|
||||
"description": "Удалить \"{{ app }}\" и все его данные?"
|
||||
},
|
||||
"updates": {
|
||||
"info": {
|
||||
@@ -354,7 +357,7 @@
|
||||
},
|
||||
"auto": {
|
||||
"title": "Автоматические обновления",
|
||||
"description": "Обновления приложения устанавливаются периодически в соответствии с Расписанием обновлений."
|
||||
"description": "Обновления приложения применяются периодически, в соответствии с <a href=\"/#/system-update\">расписанием обновлений</a>"
|
||||
},
|
||||
"updates": {
|
||||
"description": "Cloudron периодически проверяет Магазин приложений на наличие обновлений."
|
||||
@@ -379,7 +382,7 @@
|
||||
},
|
||||
"auto": {
|
||||
"title": "Автоматические резервные копии",
|
||||
"description": "Резервное копирование приложения осуществляется периодически в соответствии с Расписанием резервного копирования."
|
||||
"description": "Периодическое создание резервных копий приложения в настроенные <a href=\"/#/backup-sites\">Локации резервных копий</a>"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
@@ -411,6 +414,9 @@
|
||||
"dashboardVisibility": "Видимость в панели управления",
|
||||
"visibleForAllUsers": "Отображается для всех пользователей Cloudron",
|
||||
"visibleForSelected": "Отображается только для выбранных пользователей и групп"
|
||||
},
|
||||
"dashboardVisibility": {
|
||||
"description": "Настройте, кто сможет видеть это приложение в панели управления."
|
||||
}
|
||||
},
|
||||
"logsActionTooltip": "Логи",
|
||||
@@ -446,7 +452,8 @@
|
||||
"description": "Максимальный процент CPU, который может быть задействован в работе приложения"
|
||||
},
|
||||
"devices": {
|
||||
"label": "Устройства"
|
||||
"label": "Устройства",
|
||||
"description": "Список подключенных к приложению устройств, через запятую"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
@@ -527,6 +534,9 @@
|
||||
"enable": "Использовать электронную почту Cloudron для получения писем",
|
||||
"disableDescription": "Данное приложение не использует настройки электронной почты Cloudron. Вы можете настроить её внутри приложения. Выберите данную опцию, если электронная почта домена находится на отдельном сервере.",
|
||||
"enableDescription": "Приложение настроено на отправку писем с использованием указанного адреса. Выберите данную, если электронная почта {{ domain }} находится на данном сервере."
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Исходящая почта"
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
@@ -537,7 +547,8 @@
|
||||
},
|
||||
"robots": {
|
||||
"title": "Robots.txt",
|
||||
"disableIndexingAction": "Отключить индексирование"
|
||||
"disableIndexingAction": "Отключить индексирование",
|
||||
"description": "По умолчанию, роботы могут индексировать это приложение."
|
||||
},
|
||||
"hstsPreload": "Активировать предзагрузку HSTS (в том числе для поддоменов)"
|
||||
},
|
||||
@@ -583,7 +594,9 @@
|
||||
"importAction": "Импортировать",
|
||||
"uploadAction": "загрузить Конфигурацию Резервной копии",
|
||||
"remotePath": "Путь резервной копии",
|
||||
"provideBackupInfo": "Предоставьте информации о резервной копии для восстановления или"
|
||||
"provideBackupInfo": "Предоставьте информации о резервной копии для восстановления или",
|
||||
"warning": "Любые данные, созданные с момента последней резервной копии будут навсегда утеряны. Рекомендуется создать новую резервную копию перед импортированием.",
|
||||
"versionMustMatchInfo": "Версия контейнера и настройки доступа в резервной копии должны совпадать с этим приложением."
|
||||
},
|
||||
"updateDialog": {
|
||||
"title": "Обновить {{ app }}",
|
||||
@@ -597,7 +610,7 @@
|
||||
"restoreDialog": {
|
||||
"title": "Восстановить {{ app }}",
|
||||
"restoreAction": "Восстановить",
|
||||
"description": "Данное действие восстановит данные приложения от {{ creationTime }}.",
|
||||
"description": "Восстановить \"{{ fqdn }}\" из резервной копии, созданной {{ creationTime }}?",
|
||||
"warning": "Любые данные, созданные между настоящим моментом и последней известной резервной копией будут безвозвратно утеряны. Рекомендуем создать резервную копию текущих данных перед восстановлением.",
|
||||
"cloneAction": "Клонировать",
|
||||
"cloneActionOverwrite": "Клонировать и перезаписать DNS"
|
||||
@@ -675,9 +688,14 @@
|
||||
"days": "Дни",
|
||||
"hours": "Часы",
|
||||
"retentionPolicy": "Политика хранения",
|
||||
"title": "Настроить расписание и хранение резервных копий",
|
||||
"title": "Настроить расписание & политику хранения резервных копий",
|
||||
"enable": "Включить автоматическое резервное копирование",
|
||||
"disable": "Отключить автоматическое резервное копирование"
|
||||
"disable": "Отключить автоматическое резервное копирование",
|
||||
"schedule": {
|
||||
"context": "Настроить расписание & политику хранения локации резервных копий \"{{ name }}\"",
|
||||
"title": "Расписание резервного копирования",
|
||||
"description": "Установить дни и время для запуска резервного копирования. Убедитесь, что установленное расписание не пересекается с <a href=\"/#/system-update\">расписанием обновлений</a>."
|
||||
}
|
||||
},
|
||||
"configureBackupStorage": {
|
||||
"encryptionPassword": "Пароль шифрования",
|
||||
@@ -692,23 +710,23 @@
|
||||
"bucketName": "Имя корзины",
|
||||
"prefix": "Префикс",
|
||||
"region": "Регион",
|
||||
"s3AccessKeyId": "Access Key ID",
|
||||
"s3SecretAccessKey": "Secret Access Key",
|
||||
"s3AccessKeyId": "Access key ID",
|
||||
"s3SecretAccessKey": "Secret access key",
|
||||
"gcsServiceKey": "Ключ сервисного аккаунта",
|
||||
"format": "Формат хранилища",
|
||||
"memoryLimit": "Лимит памяти",
|
||||
"encryptionDescription": "Cloudron не хранит установленный пароль на сервере. Пожалуйста, сохраните его в надёжном месте. В противном случае, dы не сможете расшифровать резервные копии",
|
||||
"memoryLimitDescription": "Лимит памяти для задачи резервного копирования. Измените это значение, если вы хотите увеличить параметр параллельности.",
|
||||
"encryptionDescription": "Cloudron не хранит установленный пароль на сервере. Пожалуйста, сохраните его в надёжном месте. В противном случае, вы не сможете расшифровать резервные копии.",
|
||||
"memoryLimitDescription": "Лимит памяти для задачи резервного копирования",
|
||||
"uploadPartSize": "Размер части копии",
|
||||
"downloadConcurrency": "Многопоточная загрузка",
|
||||
"uploadConcurrency": "Многопоточная выгрузка",
|
||||
"downloadConcurrencyDescription": "Количество файлов, загружаемых одновременно во время восстановления",
|
||||
"uploadConcurrencyDescription": "Количество файлов, выгружаемых одновременно во время резервного копирования",
|
||||
"downloadConcurrencyDescription": "Количество файлов, загружаемых одновременно",
|
||||
"uploadConcurrencyDescription": "Количество файлов, выгружаемых одновременно",
|
||||
"copyConcurrency": "Многопоточное копирование",
|
||||
"encryptionPasswordPlaceholder": "Парольная фраза, используемая для расшифровки резервных копий",
|
||||
"encryptionPasswordRepeat": "Повторите пароль",
|
||||
"server": "IP сервера или Имя хоста",
|
||||
"remoteDirectory": "Удалённый Каталог",
|
||||
"server": "IP сервера / Имя хоста",
|
||||
"remoteDirectory": "Удалённый каталог",
|
||||
"username": "Имя пользователя",
|
||||
"port": "Порт",
|
||||
"user": "Пользователь",
|
||||
@@ -716,7 +734,7 @@
|
||||
"diskPath": "Путь на диске",
|
||||
"s3LikeNote": "Убедитесь, что в хранилище отсутствуют установленные правила жизненного цикла объектов, так как они могут привести к повреждению резервных копий rsync.",
|
||||
"uploadPartSizeDescription": "Размер одной части копии, состоящей из нескольких частей. До 3-х частей загружаются параллельно и требуют больше выделенной памяти.",
|
||||
"copyConcurrencyDescription": "Количество удаленных копий файлов, выгружаемых одновременно во время резервного копирования.",
|
||||
"copyConcurrencyDescription": "Количество удаленных копий файлов, выгружаемых одновременно",
|
||||
"password": "Пароль",
|
||||
"cifsSealSupport": "Использовать SEAL шифрование (требуется SMB v3 и выше)",
|
||||
"chown": "Удалённая файловая система поддерживает chown",
|
||||
@@ -724,14 +742,15 @@
|
||||
"preserveAttributesLabel": "Сохранить атрибуты файла",
|
||||
"name": "Имя",
|
||||
"encryptionHint": "Подсказка для пароля шифрования",
|
||||
"usesEncryption": "Резервное копирование использует шифрование",
|
||||
"useForUpdates": "Сохранять резервные копии автоматических обновлений здесь",
|
||||
"usesEncryption": "Резервная копия зашифрована",
|
||||
"useForUpdates": "Сохранять резервные копии автообновлений здесь",
|
||||
"backupContents": {
|
||||
"title": "Содержание резервной копии",
|
||||
"description": "Выберите, что вы хотите сохранить в этой локации.",
|
||||
"everything": "Всё",
|
||||
"excludeSelected": "Исключить выбранное",
|
||||
"includeOnlySelected": "Включить только выбранное"
|
||||
"includeOnlySelected": "Включить только выбранное",
|
||||
"context": "Настроить содержимое резервной копии локации \"{{ name }}\""
|
||||
},
|
||||
"automaticUpdates": {
|
||||
"title": "Резервные копии автоматических обновлений",
|
||||
@@ -762,7 +781,7 @@
|
||||
"title": "Восстановить из Архива",
|
||||
"restoreActionOverwrite": "Восстановить и перезаписать DNS",
|
||||
"restoreAction": "Восстановить",
|
||||
"description": "{{appId}} восстановится на выбранный адрес из резервной копии от {{creationTime}}."
|
||||
"description": "\"{{appId}}\" восстановится на выбранный адрес из резервной копии от {{creationTime}}"
|
||||
},
|
||||
"archives": {
|
||||
"title": "Архив приложений",
|
||||
@@ -814,7 +833,7 @@
|
||||
"title": "Настройки",
|
||||
"location": "Адрес почтового сервера",
|
||||
"spamFilter": "Фильтр спама",
|
||||
"spamFilterOverview": "{{ blacklistCount }} адресов в листе блокировки.",
|
||||
"spamFilterOverview": "{{ blacklistCount }} адресов в листе блокировки",
|
||||
"acl": "Почтовый ACL (Access Control List)",
|
||||
"maxMailSize": "Максимальный размер письма",
|
||||
"solrFts": "Полнотекстовый поиск",
|
||||
@@ -849,10 +868,11 @@
|
||||
"rcptTo": "К"
|
||||
},
|
||||
"changeDomainDialog": {
|
||||
"description": "Данное действие перенесёт IMAP и SMTP сервер в указанное расположение."
|
||||
"description": "Установить IMAP и SMTP сервер в указанное расположение",
|
||||
"setAction": "Установить локацию"
|
||||
},
|
||||
"changeMailSizeDialog": {
|
||||
"description": "Изменение максимального размера письма требует перезагрузки почтового сервера."
|
||||
"description": "Входящие письма больше установленного размера будут отклоняться"
|
||||
},
|
||||
"spamFilterDialog": {
|
||||
"title": "Фильтрация спама",
|
||||
@@ -863,7 +883,7 @@
|
||||
"blacklisteAddressesInfo": "Подходящие адреса будут попадать в папку Спам. Поддерживаются '*' и '?' шаблоны glob."
|
||||
},
|
||||
"testMailDialog": {
|
||||
"title": "Отправить тестовое письмо для {{ domain }}",
|
||||
"title": "Отправить тестовое письмо",
|
||||
"description": "Будет отправлено тестовое писсьмо от <b>no-reply@{{ domain }}</b> на адреса, указанные ниже.",
|
||||
"sendAction": "Отправить"
|
||||
},
|
||||
@@ -872,13 +892,13 @@
|
||||
},
|
||||
"typeFilterHeader": "Все события",
|
||||
"aclDialog": {
|
||||
"dnsblZones": "DNSBL Зоны",
|
||||
"dnsblZones": "DNSBL зоны",
|
||||
"dnsblZonesInfo": "Подключающийся IP адрес проверяется в этих списках заблокированных IP",
|
||||
"dnsblZonesPlaceholder": "Названия зон, разделённые линиями",
|
||||
"title": "Изменить ACL электронной почты"
|
||||
},
|
||||
"mailboxSharing": {
|
||||
"description": "Если активировано, пользователи смогут открывать доступ к своим IMAP папкам для других пользователей.",
|
||||
"description": "Если активировано, пользователи смогут открывать доступ к своим IMAP папкам для других пользователей",
|
||||
"title": "Общедоступный почтовый ящик"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
@@ -941,9 +961,10 @@
|
||||
"description": "Службы обеспечивают работу таких систем, как базы данных, электронная почта и авторизация.",
|
||||
"restartActionTooltip": "Перезагрузить",
|
||||
"configure": {
|
||||
"title": "Настроить {{ name }}",
|
||||
"title": "Настроить службу",
|
||||
"resetToDefaults": "Сбросить к стандартным настройкам",
|
||||
"enableRecoveryMode": "Включить режим восстановления"
|
||||
"enableRecoveryMode": "Включить режим восстановления",
|
||||
"description": "Настроить службу \"{{ name }}\""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -959,7 +980,7 @@
|
||||
"subscriptionReactivateAction": "Реактивировать подписку",
|
||||
"emailNotVerified": "Электронная почта не подтверждена",
|
||||
"account": "Аккаунт",
|
||||
"unlinkAction": "Отвязать Аккаунт",
|
||||
"unlinkAction": "Отвязать аккаунт",
|
||||
"unlinkDialog": {
|
||||
"title": "Отвязать Аккаунт Cloudron.io",
|
||||
"description": "Данное действие отвяжет этот Cloudron от действующего аккаунта Cloudron.io. После он может быть <a href=\"https://docs.cloudron.io/appstore/#account-change\" target=\"_blank\">привязан</a> к другому аккаунту."
|
||||
@@ -972,7 +993,7 @@
|
||||
"updates": {
|
||||
"title": "Обновления",
|
||||
"checkForUpdatesAction": "Проверить обновления",
|
||||
"updateAvailableAction": "Доступно Обновление",
|
||||
"updateAvailableAction": "Доступно обновление",
|
||||
"stopUpdateAction": "Остановить обновление",
|
||||
"description": "Обновления платформы и приложений запускаются на основании установленного расписания и в соответствии с <a href=\"/#/system-settings\">системным часовым поясом</a>.",
|
||||
"schedule": "Расписание обновлений",
|
||||
@@ -989,20 +1010,21 @@
|
||||
"description": "Установите дни и часы, в которые будет происходить автоматическое обновление платформы и приложений. Убедитесь, что установленное расписание не пересекается с расписанием резервного копирования."
|
||||
},
|
||||
"updateDialog": {
|
||||
"title": "Обновить Cloudron до",
|
||||
"title": "Обновить Cloudron",
|
||||
"blockingAppsInfo": "Пожалуйста, ожидайте завершения указанных операций.",
|
||||
"unstableWarning": "Данное обновление является пред-релизным и не может гарантировать полную стабильность. Применяйте его на свой страх и риск.",
|
||||
"changes": "Изменения",
|
||||
"skipBackupCheckbox": "Пропустить резервное копирование",
|
||||
"updateAction": "Обновить",
|
||||
"blockingApps": "Эти приложения блокируют обновления, потому что у них есть незавершённые действия:"
|
||||
"blockingApps": "Эти приложения блокируют обновления, потому что у них есть незавершённые действия:",
|
||||
"updateAvailable": "Доступен Cloudron {{ newVersion }}"
|
||||
},
|
||||
"language": {
|
||||
"title": "Язык",
|
||||
"description": "Устанавливает язык по умолчанию для Cloudron и системных писем (в том числе для приглашений, сброса пароля и др.). Пользователи могут изменить язык панели управления в своём профиле."
|
||||
},
|
||||
"registryConfig": {
|
||||
"provider": "Провайдер Реестра Docker",
|
||||
"provider": "Провайдер Docker реестра",
|
||||
"providerOther": "Другое"
|
||||
}
|
||||
},
|
||||
@@ -1119,7 +1141,8 @@
|
||||
"gandiTokenTypePAT": "Персональный токен доступа (PAT)",
|
||||
"inwxUsername": "Имя пользователя",
|
||||
"inwxPassword": "Пароль",
|
||||
"customNameservers": "Домен использует пользовательские (Vanity) серверы имён"
|
||||
"customNameservers": "Домен использует пользовательские (Vanity) серверы имён",
|
||||
"zoneNamePlaceholder": "Необязательно. Если не указано, используется корневой домен."
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Удалить домен",
|
||||
@@ -1155,7 +1178,7 @@
|
||||
},
|
||||
"allCaughtUp": "Уведомления отсутствуют",
|
||||
"settingsDialog": {
|
||||
"description": "Для выбранных событий уведомления будут отправляться на главный email."
|
||||
"description": "Для выбранных событий уведомления будут отправляться на основной email."
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
@@ -1166,7 +1189,9 @@
|
||||
"terminal": {
|
||||
"title": "Терминал",
|
||||
"download": {
|
||||
"download": "Скачать"
|
||||
"download": "Скачать",
|
||||
"title": "Скачать файл",
|
||||
"description": "Введите путь к файлу или каталогу для скачивания из файловой системы приложения."
|
||||
},
|
||||
"scheduler": "Планировщик/Cron",
|
||||
"downloadAction": "Скачать",
|
||||
@@ -1284,8 +1309,8 @@
|
||||
"spfDocInfo": "Cloudron не настраивает SPF запись автоматически. Для ручной настройки советуем ознакомиться с <a href=\"{{ spfDocsLink }}\" target=\"_blank\">{{ name }} документацией</a>."
|
||||
},
|
||||
"title": "Ретранслятор почты",
|
||||
"noopNonAdminDomainWarning": "Cloudron не сможет обеспечить отправку писем для приложений, размещенных на этом домене, если электронная почта выключена.",
|
||||
"description": "Этот почтовый сервер (смарт-хост) будет использоваться для отправки исходящей почты приложений, установленных под выбранным доменом.",
|
||||
"noopNonAdminDomainWarning": "Электронные письма не будут отправляться с этого домена",
|
||||
"description": "Настроить исходящую почту для этого домена",
|
||||
"noopAdminDomainWarning": "Cloudron не сможет отправлять приглашения, ссылки для сброса пароля и другие уведомления, если электронная почта выключена на основном домене"
|
||||
},
|
||||
"dnsStatus": {
|
||||
@@ -1301,10 +1326,10 @@
|
||||
},
|
||||
"enableEmailDialog": {
|
||||
"noProviderInfo": "Провайдер DNS не настроен. Записи DNS, указанные на вкладке Статус, должны быть настроены вручную.",
|
||||
"title": "Включить электронную почту для {{ domain }}?",
|
||||
"title": "Включить входящую почту",
|
||||
"setupDnsCheckbox": "Установить почтовые DNS записи",
|
||||
"enableAction": "Включить",
|
||||
"description": "Данный параметр настроит Cloudron на получение писем для <b>{{ domain }}</b>. Рекомендуем ознакомиться с <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">документацией</a> для открытия необходимых почтовому серверу портов.",
|
||||
"description": "Cloudron начнёт получать электронную почту для \"{{ domain }}\". Смотрите документацию для <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">требуемых портов</a>.",
|
||||
"setupDnsInfo": "Используйте данную опцию, чтобы автоматически настроить относящиеся к электронной почте записи DNS. Вы можете не отмечать её сразу, чтобы предварительно создать почтовые ящики и <a href=\"{{ importEmailDocsLink }}\">импортировать письма</a>."
|
||||
},
|
||||
"incoming": {
|
||||
@@ -1336,14 +1361,14 @@
|
||||
"catchall": {
|
||||
"title": "Catch-all переадресация",
|
||||
"saveAction": "Сохранить",
|
||||
"description": "Письма, отправленные на несуществующие адреса, будут переадресованы на выбранные почтовые ящики."
|
||||
"description": "Письма, отправленные на несуществующие адреса, будут переадресованы на выбранные почтовые ящики"
|
||||
},
|
||||
"incomingServerInfo": "Входящая почта (IMAP)",
|
||||
"howToConnectDescription": "Используйте данные ниже, чтобы настроить почтовые клиенты.",
|
||||
"incomingUserInfo": "Имя пользователя",
|
||||
"incomingPasswordInfo": "Пароль",
|
||||
"incomingPasswordUsage": "Пароль владельца почтового ящика",
|
||||
"description": "Получать входящие письма для этого домена."
|
||||
"description": "Получать входящие письма для этого домена"
|
||||
},
|
||||
"config": {
|
||||
"title": "Конфигурация электронной почты {{ domain }}",
|
||||
@@ -1359,7 +1384,9 @@
|
||||
"title": "Email подпись",
|
||||
"plainTextFormat": "Обычный текст",
|
||||
"htmlFormat": "Формат HTML",
|
||||
"description": "Данный текст будет прикреплён ко всем письмам, отправляемым с выбранного домена."
|
||||
"description": "Данный текст будет прикреплён ко всем письмам, отправляемым с выбранного домена.",
|
||||
"customSignatureSet": "Настроена пользовательская подпись",
|
||||
"noSignatureSet": "Подпись не настроена"
|
||||
},
|
||||
"smtpStatus": {
|
||||
"notBlacklisted": "IP-адрес сервера {{ ip }} <b>не</b> обнаружен в списках заблокированных.",
|
||||
@@ -1369,13 +1396,13 @@
|
||||
},
|
||||
"disableEmailDialog": {
|
||||
"disableAction": "Выключить",
|
||||
"title": "Выключить сервер электронной почты для {{ domain }}?",
|
||||
"title": "Выключить входящую почту",
|
||||
"description": "Cloudron перестанет получать электронные письма для <b>{{ domain }}</b>. Почтовые ящики и листы рассылок для данного домена не будут удалены."
|
||||
},
|
||||
"addMailboxDialog": {
|
||||
"title": "Добавить почтовый ящик",
|
||||
"name": "Имя",
|
||||
"incomingDisabledWarning": "Для этого домена входящая электронная почта не включена."
|
||||
"incomingDisabledWarning": "Для этого домена входящая электронная почта не включена"
|
||||
},
|
||||
"editMailboxDialog": {
|
||||
"title": "Редактировать почтовый ящик {{ name }}@{{ domain }}",
|
||||
@@ -1390,7 +1417,7 @@
|
||||
"title": "Удалить почтовый ящик {{ name }}@{{ domain }}",
|
||||
"deleteAction": "Удалить",
|
||||
"purgeMailboxCheckbox": "Удалить все письма и фильтры внутри этого почтового ящика",
|
||||
"description": "После удаления, письма, отправленные на данный почтовый ящик, будут возвращаться отправителю с ошибкой. Вы можете не удалять содержимое почтовых ящиков в архивных целях. Они будут храниться на сервере по пути <code> /home/yellowtent/boxdata/mail/vmail</code>."
|
||||
"description": "После удаления, письма, отправленные на данный почтовый ящик, будут возвращаться отправителю. Вы можете не удалять почту в архивных целях. Она будут храниться на сервере по пути \"/home/yellowtent/boxdata/mail/vmail\".<br/><br/>Удалить \"{{ name }}@{{ domain }}\"?"
|
||||
},
|
||||
"addMailinglistDialog": {
|
||||
"name": "Имя",
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<title><%= name %> Account Setup</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
window.cloudron = <%- JSON.stringify({
|
||||
name: name,
|
||||
footer: footer,
|
||||
language: language
|
||||
}) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { Dialog, FormGroup, TextInput, PasswordInput, Checkbox } from '@cloudron/pankow';
|
||||
import { s3like, mountlike } from '../utils.js';
|
||||
import { s3like, mountlike, parseFullBackupPath } from '../utils.js';
|
||||
import BackupProviderForm from './BackupProviderForm.vue';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import { REGIONS_CONTABO, REGIONS_VULTR, REGIONS_IONOS, REGIONS_OVH, REGIONS_LINODE, REGIONS_SCALEWAY, REGIONS_WASABI } from '../constants.js';
|
||||
@@ -17,28 +17,35 @@ const busy = ref(false);
|
||||
const formError = ref({});
|
||||
const providerConfig = ref({});
|
||||
const provider = ref('');
|
||||
const remotePath = ref('');
|
||||
const fullPath = ref('');
|
||||
const format = ref('');
|
||||
const encrypted = ref(false);
|
||||
const encryptionPasswordHint = ref('');
|
||||
const encryptionPassword = ref('');
|
||||
const encryptedFilenames = ref(false);
|
||||
|
||||
const isFormValid = ref(false);
|
||||
async function validateForm() {
|
||||
isFormValid.value = form.value && form.value.checkValidity();
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!form.value.reportValidity()) return;
|
||||
|
||||
formError.value = {};
|
||||
busy.value = true;
|
||||
|
||||
let backupPath = remotePath.value;
|
||||
const config = {};
|
||||
|
||||
const { prefix, remotePath } = parseFullBackupPath(fullPath.value);
|
||||
|
||||
// only set provider specific fields, this will clear them in the db
|
||||
if (s3like(provider.value)) {
|
||||
config.bucket = providerConfig.value.bucket;
|
||||
config.prefix = providerConfig.value.prefix;
|
||||
config.accessKeyId = providerConfig.value.accessKeyId;
|
||||
config.secretAccessKey = providerConfig.value.secretAccessKey;
|
||||
config.prefix = prefix;
|
||||
|
||||
if (providerConfig.value.endpoint) config.endpoint = providerConfig.value.endpoint;
|
||||
|
||||
@@ -85,7 +92,7 @@ async function onSubmit() {
|
||||
config.signatureVersion = 'v4';
|
||||
}
|
||||
} else if (mountlike(provider.value)) {
|
||||
config.prefix = providerConfig.value.prefix;
|
||||
config.prefix = prefix;
|
||||
config.noHardlinks = !providerConfig.value.useHardlinks;
|
||||
config.mountOptions = {};
|
||||
|
||||
@@ -113,21 +120,19 @@ async function onSubmit() {
|
||||
config.preserveAttributes = !!providerConfig.value.preserveAttributes;
|
||||
}
|
||||
} else if (provider.value === 'filesystem') {
|
||||
const parts = remotePath.value.split('/');
|
||||
backupPath = parts.pop() || parts.pop(); // removes any trailing slash. this is basename()
|
||||
config.backupDir = parts.join('/'); // this is dirname()
|
||||
config.backupDir = prefix;
|
||||
} else if (provider.value === 'gcs') {
|
||||
config.bucket = providerConfig.value.bucket;
|
||||
config.prefix = providerConfig.value.prefix;
|
||||
config.projectId = providerConfig.value.projectId;
|
||||
config.credentials = providerConfig.value.credentials;
|
||||
config.prefix = prefix;
|
||||
}
|
||||
|
||||
const data = {
|
||||
format: format.value,
|
||||
provider: provider.value,
|
||||
config: config,
|
||||
remotePath: backupPath
|
||||
config,
|
||||
remotePath
|
||||
};
|
||||
|
||||
if (encrypted.value) {
|
||||
@@ -188,37 +193,47 @@ function onBackupConfigChanged(event) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(result.target.result); // 'provider', 'config', 'limits', 'format', 'remotePath', 'encrypted', 'encryptedFilenames'
|
||||
if (data.provider === 'filesystem') { // this allows a user to upload a backup to server and import easily with an absolute path
|
||||
data.remotePath = `${data.config.backupDir}/${data.remotePath}`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Unable to parse backup config', e);
|
||||
return;
|
||||
}
|
||||
|
||||
provider.value = data.provider;
|
||||
remotePath.value = data.remotePath;
|
||||
if (data.provider === 'filesystem') { // this allows a user to upload a backup to server and import easily with an absolute path
|
||||
fullPath.value = data.config.prefix ? `${data.config.backupDir}/${data.config.prefix}/${data.remotePath}` : `${data.config.backupDir}/${data.remotePath}`;
|
||||
} else if (data.provider === 'mountpoint') {
|
||||
fullPath.value = data.config.prefix ? `${data.config.mountPoint}/${data.config.prefix}/${data.remotePath}` : `${data.config.mountPoint}/${data.remotePath}`;
|
||||
} else {
|
||||
fullPath.value = data.config.prefix ? `${data.config.prefix}/${data.remotePath}` : data.remotePath;
|
||||
}
|
||||
format.value = data.format;
|
||||
encrypted.value = !!data.encrypted;
|
||||
encryptionPasswordHint.value = data.encryptionPasswordHint || '';
|
||||
encryptionPassword.value = '';
|
||||
encryptedFilenames.value = data.encryptedFilenames;
|
||||
|
||||
// translate for BackupProviderForm flattened object
|
||||
providerConfig.value = {};
|
||||
providerConfig.value.useHardlinks = !data.config.noHardlinks;
|
||||
providerConfig.value.prefix = data.config.prefix;
|
||||
providerConfig.value.chown = !!data.config.chown;
|
||||
providerConfig.value.preserveAttributes = data.config.preserveAttributes;
|
||||
providerConfig.value.mountOptionHost = data.config.mountOptions.host;
|
||||
providerConfig.value.mountOptionPort = data.config.mountOptions.port;
|
||||
providerConfig.value.mountOptionRemoteDir = data.config.mountOptions.remoteDir;
|
||||
providerConfig.value.mountOptionSeal = !!data.config.mountOptions.seal;
|
||||
providerConfig.value.mountOptionDiskPath = data.config.mountOptions.diskPath;
|
||||
providerConfig.value.mountOptionUser = data.config.mountOptions.user;
|
||||
providerConfig.value.mountOptionUsername = data.config.mountOptions.username;
|
||||
providerConfig.value.mountOptionPassword = data.config.mountOptions.password;
|
||||
providerConfig.value.mountOptionPrivateKey = '';
|
||||
for (const [key, value] of Object.entries(data.config)) {
|
||||
if (key === 'noHardlinks' || key === 'chown' || key === 'preserveAttributes') {
|
||||
// not really used for importing
|
||||
} else if (key === 'mountOptions') { // providerConfig uses a flattened format of config.mountOptions
|
||||
providerConfig.value.mountOptionHost = data.config.mountOptions.host;
|
||||
providerConfig.value.mountOptionPort = data.config.mountOptions.port;
|
||||
providerConfig.value.mountOptionRemoteDir = data.config.mountOptions.remoteDir;
|
||||
providerConfig.value.mountOptionSeal = !!data.config.mountOptions.seal;
|
||||
providerConfig.value.mountOptionDiskPath = data.config.mountOptions.diskPath;
|
||||
providerConfig.value.mountOptionUser = data.config.mountOptions.user;
|
||||
providerConfig.value.mountOptionUsername = data.config.mountOptions.username;
|
||||
providerConfig.value.mountOptionPassword = data.config.mountOptions.password;
|
||||
providerConfig.value.mountOptionPrivateKey = '';
|
||||
} else {
|
||||
// s3: 'accessKeyId', 'secretAccessKey', 'bucket', 'prefix', 'signatureVersion', 'endpoint', 'region', 'acceptSelfSignedCerts', 's3ForcePathStyle'
|
||||
// gcs: 'bucket', 'prefix'
|
||||
providerConfig.value[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(validateForm, 100); // update state of the confirm button
|
||||
};
|
||||
|
||||
reader.readAsText(event.target.files[0]);
|
||||
@@ -235,7 +250,7 @@ defineExpose({
|
||||
formError.value = {};
|
||||
provider.value = '';
|
||||
providerConfig.value = {};
|
||||
remotePath.value = '';
|
||||
fullPath.value = '';
|
||||
encrypted.value = false;
|
||||
encryptionPassword.value = '';
|
||||
encryptedFilenames.value = false;
|
||||
@@ -253,7 +268,7 @@ defineExpose({
|
||||
|
||||
<Dialog ref="dialog" :title="$t('app.importBackupDialog.title')"
|
||||
:confirm-label="$t('app.importBackupDialog.importAction')"
|
||||
:confirm-active="!busy"
|
||||
:confirm-active="!busy && isFormValid"
|
||||
:confirm-busy="busy"
|
||||
:reject-label="$t('main.dialog.cancel')"
|
||||
:reject-active="!busy"
|
||||
@@ -273,14 +288,14 @@ defineExpose({
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="onSubmit()" autocomplete="off" ref="form">
|
||||
<form @submit.prevent="onSubmit()" autocomplete="off" ref="form" @input="validateForm()">
|
||||
<fieldset :disabled="busy">
|
||||
<input style="display: none;" type="submit"/>
|
||||
|
||||
<!-- remotePath contains the prefix as well -->
|
||||
<FormGroup>
|
||||
<label for="inputRemotePath">{{ $t('app.importBackupDialog.remotePath') }} <sup><a href="https://docs.cloudron.io/backups/#import-app-backup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<TextInput id="inputRemotePath" v-model="remotePath" required />
|
||||
<TextInput id="inputRemotePath" v-model="fullPath" required />
|
||||
</FormGroup>
|
||||
|
||||
<BackupProviderForm ref="form"
|
||||
|
||||
@@ -195,7 +195,7 @@ onMounted(async () => {
|
||||
<!-- S3/Minio/SOS/GCS/UpCloud/B2/R2 -->
|
||||
<FormGroup v-if="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
<label for="endpointInput">{{ $t('backups.configureBackupStorage.s3Endpoint') }}</label>
|
||||
<TextInput id="endpointInput" v-model="providerConfig.endpoint" placeholder="URL" required />
|
||||
<TextInput id="endpointInput" v-model="providerConfig.endpoint" placeholder="https://s3endpoint.example.com" required />
|
||||
</FormGroup>
|
||||
|
||||
<Checkbox v-if="provider === 'minio' || provider === 's3-v4-compat'" v-model="providerConfig.acceptSelfSignedCerts" :label="$t('backups.configureBackupStorage.acceptSelfSignedCerts')"/>
|
||||
@@ -205,6 +205,7 @@ onMounted(async () => {
|
||||
<TextInput id="bucketInput" v-model="providerConfig.bucket" required />
|
||||
</FormGroup>
|
||||
|
||||
<!-- when importing/restoring, the user enters a fullPath which contains the prefix -->
|
||||
<FormGroup v-if="provider !== 'filesystem' && !importOnly">
|
||||
<label for="prefixInput">{{ $t('backups.configureBackupStorage.prefix') }}</label>
|
||||
<TextInput id="prefixInput" v-model="providerConfig.prefix" placeholder="Prefix for backup file names" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ref, useTemplateRef, watch } from 'vue';
|
||||
import { MaskedInput, Dialog, FormGroup, TextInput, Checkbox } from '@cloudron/pankow';
|
||||
import { prettyBinarySize } from '@cloudron/pankow/utils';
|
||||
import { s3like, mountlike, regionName } from '../utils.js';
|
||||
import { s3like, mountlike, prettySiteLocation } from '../utils.js';
|
||||
import BackupSitesModel from '../models/BackupSitesModel.js';
|
||||
import SystemModel from '../models/SystemModel.js';
|
||||
|
||||
@@ -205,15 +205,7 @@ defineExpose({
|
||||
|
||||
<FormGroup v-if="site.provider && site.config">
|
||||
<label><i v-if="site.encrypted" class="fa-solid fa-lock"></i> Storage: <b>{{ site.provider }} ({{ site.format }}) </b></label>
|
||||
<div>
|
||||
<span v-if="site.provider === 'filesystem'">{{ site.config.backupDir }}{{ (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'disk' || site.provider === 'ext4' || site.provider === 'xfs' || site.provider === 'mountpoint'">{{ site.config.mountOptions.diskPath || site.config.mountPoint }}{{ (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'cifs' || site.provider === 'nfs' || site.provider === 'sshfs'">{{ site.config.mountOptions.host }}:{{ site.config.mountOptions.remoteDir }}{{ (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 's3'">{{ site.config.region + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'minio'">{{ site.config.endpoint + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'gcs'">{{ site.config.endpoint + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else>{{ regionName(site.provider, site.config.endpoint) + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
</div>
|
||||
<div>{{ prettySiteLocation(site) }}</div>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="provider === 'sshfs'">
|
||||
|
||||
@@ -314,6 +314,7 @@ function onGcdnsFileInputChange(event) {
|
||||
<FormGroup v-if="showAdvanced">
|
||||
<label for="zoneNameInput">{{ $t('domains.domainDialog.zoneName') }} <sup><a href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<TextInput id="zoneNameInput" v-model="zoneName" />
|
||||
<small class="helper-text">{{ $t('domains.domainDialog.zoneNamePlaceholder') }}</small>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ const group = ref(null);
|
||||
const busy = ref(false);
|
||||
const formError = ref({});
|
||||
const name = ref('');
|
||||
const users = ref([]);
|
||||
const userIds = ref([]);
|
||||
const allUsers = ref([]);
|
||||
const apps = ref([]);
|
||||
const appIds = ref([]);
|
||||
const allApps = ref([]);
|
||||
|
||||
async function onSubmit() {
|
||||
@@ -29,7 +29,7 @@ async function onSubmit() {
|
||||
formError.value = {};
|
||||
|
||||
if (group.value) {
|
||||
const [error] = await groupsModel.update(group.value.id, name.value, users.value, apps.value);
|
||||
const [error] = await groupsModel.update(group.value.id, name.value, userIds.value, appIds.value);
|
||||
if (error) {
|
||||
if (error.body && error.body.message.indexOf('name') === 0) formError.value.name = error.body.message;
|
||||
else formError.value.generic = error.body ? error.body.message : 'Internal error';
|
||||
@@ -37,7 +37,7 @@ async function onSubmit() {
|
||||
return console.error(error);
|
||||
}
|
||||
} else {
|
||||
const [error] = await groupsModel.add(name.value, users.value, apps.value);
|
||||
const [error] = await groupsModel.add(name.value, userIds.value, appIds.value);
|
||||
if (error) {
|
||||
if (error.body && error.body.message.indexOf('name') === 0) formError.value.name = error.body.message;
|
||||
else formError.value.generic = error.body ? error.body.message : 'Internal error';
|
||||
@@ -63,13 +63,13 @@ defineExpose({
|
||||
if (error) return console.error(error);
|
||||
result.forEach(u => u.label = (u.username || u.email));
|
||||
allUsers.value = result;
|
||||
users.value = g ? g.userIds : [];
|
||||
userIds.value = g ? g.userIds : [];
|
||||
|
||||
[error, result] = await appsModel.list();
|
||||
if (error) return console.error(error);
|
||||
result.forEach(a => a.label = (a.label || a.fqdn));
|
||||
allApps.value = result;
|
||||
apps.value = g ? g.appIds : [];
|
||||
appIds.value = g ? g.appIds : [];
|
||||
|
||||
dialog.value.open();
|
||||
}
|
||||
@@ -103,13 +103,14 @@ defineExpose({
|
||||
|
||||
<FormGroup>
|
||||
<label for="usersInput">{{ $t('users.group.users') }}</label>
|
||||
<!-- membership of external groups cannot be edited -->
|
||||
<div v-if="group?.source"><span v-for="user of groupEdit.selectedUsers" :key="user.id"> {{ (user.username || user.email) }}</span></div>
|
||||
<MultiSelect v-else v-model="users" :options="allUsers" option-key="id" :search-threshold="20"/>
|
||||
<MultiSelect v-else v-model="userIds" :options="allUsers" option-key="id" :search-threshold="20"/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="appsInput">{{ $t('users.group.allowedApps') }}</label>
|
||||
<MultiSelect v-model="apps" :options="allApps" option-key="id" :search-threshold="20"/>
|
||||
<MultiSelect v-model="appIds" :options="allApps" option-key="id" :search-threshold="20"/>
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -192,7 +192,6 @@ defineExpose({
|
||||
<Button tool danger icon="fa-solid fa-trash-alt" @click="onRemoveAlias(index)"/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div class="error-label" v-if="formError">{{ formError }}</div>
|
||||
<div style="margin-top: 5px"></div>
|
||||
<div v-if="aliases.length === 0">
|
||||
{{ $t('email.editMailboxDialog.noAliases') }} <span class="actionable" @click="onAddAlias($event)">{{ $t('email.editMailboxDialog.addAliasAction') }}</span>
|
||||
|
||||
@@ -40,8 +40,7 @@ const fallbackEmail = ref('');
|
||||
const avatarUrl = ref('');
|
||||
const username = ref('');
|
||||
const role = ref('');
|
||||
const groups = ref([]);
|
||||
const localGroups = ref([]);
|
||||
const localGroupIds = ref([]);
|
||||
const allGroups = ref([]);
|
||||
const allLocalGroups = ref([]);
|
||||
const active = ref(true);
|
||||
@@ -146,7 +145,7 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
const [groupError] = await usersModel.setLocalGroups(userId, localGroups.value);
|
||||
const [groupError] = await usersModel.setLocalGroups(userId, localGroupIds.value);
|
||||
if (groupError) {
|
||||
formError.value.generic = groupError.body ? groupError.body.message : 'Internal error';
|
||||
busy.value = false;
|
||||
@@ -203,8 +202,7 @@ defineExpose({
|
||||
result.forEach(g => g.label = g.name);
|
||||
allGroups.value = result;
|
||||
allLocalGroups.value = result.filter(g => !g.source);
|
||||
groups.value = u ? u.groupIds : [];
|
||||
localGroups.value = (u ? u.groupIds.filter(g => !g.source) : []);
|
||||
localGroupIds.value = u ? u.groupIds.filter(gid => allLocalGroups.value.find(g => g.id === gid)) : [];
|
||||
|
||||
[error, result] = await profileModel.get();
|
||||
if (error) return console.error(error);
|
||||
@@ -295,7 +293,7 @@ defineExpose({
|
||||
<FormGroup>
|
||||
<label for="groupsInput">{{ $t('users.user.groups') }}</label>
|
||||
<div v-if="allGroups.length === 0">{{ $t('users.user.noGroups') }}</div>
|
||||
<MultiSelect v-if="allLocalGroups.length" v-model="localGroups" option-key="id" :options="allLocalGroups" :search-threshold="20" />
|
||||
<MultiSelect v-if="allLocalGroups.length" v-model="localGroupIds" option-key="id" :options="allLocalGroups" :search-threshold="20" />
|
||||
</FormGroup>
|
||||
|
||||
<!-- on add, this is hidden for now, until we figure why one would want to add an inactive user -->
|
||||
|
||||
+50
-10
@@ -43,6 +43,30 @@ function regionName(provider, endpoint) {
|
||||
return region.name;
|
||||
}
|
||||
|
||||
function prettySiteLocation(site) {
|
||||
switch (site.provider) {
|
||||
case 'filesystem':
|
||||
return site.config.backupDir + (site.config.prefix ? `/${site.config.prefix}` : '');
|
||||
case 'disk':
|
||||
case 'ext4':
|
||||
case 'xfs':
|
||||
case 'mountpoint':
|
||||
return (site.config.mountOptions.diskPath || site.config.mountPoint) + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 'cifs':
|
||||
case 'nfs':
|
||||
case 'sshfs':
|
||||
return site.config.mountOptions.host + ':' + site.config.mountOptions.remoteDir + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 's3':
|
||||
return site.config.region + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 'minio':
|
||||
return site.config.endpoint + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 'gcs':
|
||||
return site.config.endpoint + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
default:
|
||||
return regionName(site.provider, site.config.endpoint) + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
}
|
||||
}
|
||||
|
||||
function eventlogDetails(eventLog, app = null, appIdContext = '') {
|
||||
const ACTION_ACTIVATE = 'cloudron.activate';
|
||||
const ACTION_PROVISION = 'cloudron.provision';
|
||||
@@ -155,11 +179,6 @@ function eventlogDetails(eventLog, app = null, appIdContext = '') {
|
||||
return pre + (app.label || app.fqdn || app.subdomain) + ' (' + app.manifest.title + ') ';
|
||||
}
|
||||
|
||||
function eventBy() {
|
||||
if (eventLog.source && eventLog.source.username) return ' by ' + eventLog.source.username;
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (eventLog.action) {
|
||||
case ACTION_ACTIVATE:
|
||||
return 'Cloudron was activated';
|
||||
@@ -234,7 +253,7 @@ function eventlogDetails(eventLog, app = null, appIdContext = '') {
|
||||
|
||||
case ACTION_APP_INSTALL:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed ' + appName('at', data.app) + eventBy();
|
||||
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed ' + appName('at', data.app);
|
||||
|
||||
case ACTION_APP_RESTORE:
|
||||
if (!data.app) return '';
|
||||
@@ -720,13 +739,31 @@ function getColor(numOfSteps, step) {
|
||||
return `hsl(${deg*step} 70% 50%)`;
|
||||
}
|
||||
|
||||
// split path into a prefix (anything before timestamp or 'snapshot') and the remaining remotePath
|
||||
function parseFullBackupPath(fullPath) {
|
||||
const parts = fullPath.split('/');
|
||||
const timestampRegex = /^\d{4}-\d{2}-\d{2}-\d{6}-\d{3}$/; // timestamp (tag)
|
||||
|
||||
const idx = parts.findIndex(p => timestampRegex.test(p) || p === 'snapshot');
|
||||
|
||||
let remotePath, prefix;
|
||||
if (idx === -1) {
|
||||
remotePath = parts.pop() || parts.pop(); // if fs+rsync there may be a trailing slash, so this removes it. this is basename()
|
||||
prefix = parts.join('/'); // this is dirname()
|
||||
} else {
|
||||
prefix = parts.slice(0, idx).join('/');
|
||||
remotePath = parts.slice(idx).join('/');
|
||||
}
|
||||
|
||||
return { prefix, remotePath };
|
||||
}
|
||||
|
||||
// named exports
|
||||
export {
|
||||
prettyRelayProviderName,
|
||||
download,
|
||||
mountlike,
|
||||
s3like,
|
||||
regionName,
|
||||
eventlogDetails,
|
||||
eventlogSource,
|
||||
taskNameFromInstallationState,
|
||||
@@ -738,7 +775,9 @@ export {
|
||||
cronHours,
|
||||
getColor,
|
||||
prettySchedule,
|
||||
parseSchedule
|
||||
parseSchedule,
|
||||
prettySiteLocation,
|
||||
parseFullBackupPath
|
||||
};
|
||||
|
||||
// default export
|
||||
@@ -747,7 +786,6 @@ export default {
|
||||
download,
|
||||
mountlike,
|
||||
s3like,
|
||||
regionName,
|
||||
eventlogDetails,
|
||||
eventlogSource,
|
||||
taskNameFromInstallationState,
|
||||
@@ -759,5 +797,7 @@ export default {
|
||||
cronHours,
|
||||
getColor,
|
||||
prettySchedule,
|
||||
parseSchedule
|
||||
parseSchedule,
|
||||
prettySiteLocation,
|
||||
parseFullBackupPath
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ onMounted(async () => {
|
||||
<input type="submit" style="display: none;" :disabled="busy || !isValid"/>
|
||||
|
||||
<FormGroup :has-error="formError.displayName">
|
||||
<label for="displayNameInput">Full Name</label>
|
||||
<label for="displayNameInput">Full name</label>
|
||||
<TextInput id="displayNameInput" v-model="displayName" required />
|
||||
<small class="text-danger">{{ formError.displayName }}</small>
|
||||
</FormGroup>
|
||||
@@ -114,11 +114,11 @@ onMounted(async () => {
|
||||
<small class="text-danger">{{ formError.password }}</small>
|
||||
</FormGroup>
|
||||
|
||||
<Checkbox v-model="acceptLicense" label="Accept Cloudron License" helpUrl="https://www.cloudron.io/legal/terms.html" required />
|
||||
<Checkbox v-model="acceptLicense" label="Accept Cloudron license" helpUrl="https://www.cloudron.io/legal/terms.html" required />
|
||||
</fieldset>
|
||||
|
||||
<div class="actions">
|
||||
<Button :disabled="busy || !isValid" :loading="busy" @click="onOwnerSubmit()">Create Admin</Button>
|
||||
<Button :disabled="busy || !isValid" :loading="busy" @click="onOwnerSubmit()">Create admin</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -134,4 +134,4 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -17,7 +17,7 @@ import SystemBackupList from '../components/SystemBackupList.vue';
|
||||
import { TASK_TYPES } from '../constants.js';
|
||||
import BackupSitesModel from '../models/BackupSitesModel.js';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
import { prettySchedule, regionName } from '../utils.js';
|
||||
import { prettySchedule, prettySiteLocation } from '../utils.js';
|
||||
|
||||
const profile = inject('profile');
|
||||
|
||||
@@ -290,15 +290,7 @@ onMounted(async () => {
|
||||
|
||||
<div>
|
||||
Storage: <b>{{ site.provider }} ({{ site.format }}) </b>
|
||||
<span>at
|
||||
<span v-if="site.provider === 'filesystem'">{{ site.config.backupDir }}{{ (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'disk' || site.provider === 'ext4' || site.provider === 'xfs' || site.provider === 'mountpoint'">{{ site.config.mountOptions.diskPath || site.config.mountPoint }}{{ (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'cifs' || site.provider === 'nfs' || site.provider === 'sshfs'">{{ site.config.mountOptions.host }}:{{ site.config.mountOptions.remoteDir }}{{ (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 's3'">{{ site.config.region + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'minio'">{{ site.config.endpoint + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else-if="site.provider === 'gcs'">{{ site.config.endpoint + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
<span v-else>{{ regionName(site.provider, site.config.endpoint) + ' ' + site.config.bucket + (site.config.prefix ? `/${site.config.prefix}` : '') }}</span>
|
||||
</span>
|
||||
<span>at {{ prettySiteLocation(site) }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -45,7 +45,7 @@ async function onSendTestMail() {
|
||||
|
||||
const address = await inputDialog.value.prompt({
|
||||
value: result.email,
|
||||
title: t('emails.testMailDialog.title', { domain: domain.value }),
|
||||
title: t('emails.testMailDialog.title'),
|
||||
message: t('emails.testMailDialog.description', { domain: domain.value }),
|
||||
confirmLabel: t('emails.testMailDialog.sendAction'),
|
||||
rejectLabel: t('main.dialog.cancel'),
|
||||
|
||||
@@ -119,7 +119,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div v-if="filteredDomains.length === 0" class="email-placeholder">{{ $t('domains.noMatchesPlaceholder') }}</div>
|
||||
<div v-if="domains.length !== 0 && filteredDomains.length === 0" class="email-placeholder">{{ $t('domains.noMatchesPlaceholder') }}</div>
|
||||
<a v-for="domain in filteredDomains" :key="domain.domain" :href="`#/email-domain/${domain.domain}`" class="email-domain">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<StateLED :busy="domain.loadingStatus" :state="domain.status ? 'success' : 'danger'"/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { ref, reactive, onMounted, watch, useTemplateRef } from 'vue';
|
||||
import { Button, TextInput, MultiSelect } from '@cloudron/pankow';
|
||||
import { useDebouncedRef, prettyEmailAddresses, prettyLongDate } from '@cloudron/pankow/utils';
|
||||
import MailModel from '../models/MailModel.js';
|
||||
@@ -23,8 +23,9 @@ const refreshBusy = ref(false);
|
||||
const eventlogs = ref([]);
|
||||
const search = useDebouncedRef('');
|
||||
const page = ref(1);
|
||||
const perPage = ref(20);
|
||||
const perPage = ref(10);
|
||||
const types = reactive([]);
|
||||
const eventlogContainer = useTemplateRef('eventlogContainer');
|
||||
|
||||
async function onRefresh() {
|
||||
refreshBusy.value = true;
|
||||
@@ -37,15 +38,17 @@ async function onRefresh() {
|
||||
refreshBusy.value = false;
|
||||
}
|
||||
|
||||
async function fetchMore() {
|
||||
page.value++;
|
||||
|
||||
const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
eventlogs.value = eventlogs.value.concat(result);
|
||||
}
|
||||
|
||||
async function onScroll(event) {
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
|
||||
page.value++;
|
||||
|
||||
const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
eventlogs.value = eventlogs.value.concat(result);
|
||||
}
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) await fetchMore();
|
||||
}
|
||||
|
||||
watch(perPage, onRefresh);
|
||||
@@ -54,6 +57,10 @@ watch(search, onRefresh);
|
||||
|
||||
onMounted(async () => {
|
||||
await onRefresh();
|
||||
|
||||
while (eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) {
|
||||
await fetchMore();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -72,7 +79,7 @@ onMounted(async () => {
|
||||
<Button tool secondary href="/logs.html?id=mail" target="_blank">{{ $t('main.action.logs') }}</Button>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="section-body" style="margin-top: 16px; overflow: auto; padding-top: 0" @scroll="onScroll">
|
||||
<div class="section-body" ref="eventlogContainer" style="margin-top: 16px; overflow: auto; padding-top: 0" @scroll="onScroll">
|
||||
<table class="eventlog-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -103,7 +110,7 @@ onMounted(async () => {
|
||||
<td class="elide-table-cell">{{ prettyEmailAddresses(eventlog.rcptTo) || eventlog.mailbox || '-' }}</td>
|
||||
<td>
|
||||
<span v-if="eventlog.type === 'bounce'">{{ $t('emails.eventlog.type.bounceInfo') }}. {{ eventlog.message || eventlog.reason }}</span>
|
||||
<span v-if="eventlog.type === 'deferred'">{{ $t('emails.eventlog.type.deferredInfo', { delay:eventlog.delay }) }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span v-if="eventlog.type === 'deferred'">{{ $t('emails.eventlog.type.deferredInfo', { delay:eventlog.delay }) }} {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span v-if="eventlog.type === 'queued'">
|
||||
<span v-if="eventlog.direction === 'inbound'">{{ $t('emails.eventlog.type.inboundInfo') }}</span>
|
||||
<span v-if="eventlog.direction === 'outbound'">{{ $t('emails.eventlog.type.outboundInfo') }}</span>
|
||||
|
||||
@@ -146,17 +146,31 @@ async function onSubmitRemove() {
|
||||
removeBusy.value = false;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
let tmp = [];
|
||||
async function refreshUsage() {
|
||||
async function refreshForDomain(domain) {
|
||||
let [error, result] = await mailModel.usage(domain);
|
||||
const [error, usage] = await mailModel.usage(domain);
|
||||
// retry if mail addon cannot be reached during restarts
|
||||
if (error && error.status === 424) return setTimeout(refresh, 2000);
|
||||
else if (error) return console.error(error);
|
||||
|
||||
const usage = result;
|
||||
mailboxes.value.forEach((m) => {
|
||||
if (usage[m.fullName]) m.usage = usage[m.fullName];
|
||||
});
|
||||
}
|
||||
|
||||
[error, result] = await mailboxesModel.list(domain);
|
||||
try {
|
||||
await eachLimit(domains.value.map(d => d.domain), 10, refreshForDomain);
|
||||
} catch (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
mailboxesUsage.value = mailboxes.value.reduce((acc, m) => acc + (m.usage && m.usage.diskSize), 0);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
let tmp = [];
|
||||
async function refreshForDomain(domain) {
|
||||
const [error, result] = await mailboxesModel.list(domain);
|
||||
if (error) throw error;
|
||||
|
||||
result.forEach((m) => {
|
||||
@@ -166,7 +180,7 @@ async function refresh() {
|
||||
if (!m.owner) m.owner = groups.value.find(g => g.id === m.ownerId) || null;
|
||||
|
||||
m.ownerDisplayName = m.owner ? (m.owner.username || m.owner.name) : '';
|
||||
m.usage = usage[m.fullName] || 0;
|
||||
m.usage = -1;
|
||||
});
|
||||
|
||||
tmp = tmp.concat(result);
|
||||
@@ -179,7 +193,6 @@ async function refresh() {
|
||||
}
|
||||
|
||||
mailboxes.value = tmp;
|
||||
mailboxesUsage.value = mailboxes.value.reduce((acc, m) => acc + (m.usage && m.usage.diskSize), 0);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -203,6 +216,9 @@ onMounted(async () => {
|
||||
|
||||
await refresh();
|
||||
|
||||
// we do this in the background to show the list faster
|
||||
refreshUsage();
|
||||
|
||||
busy.value = false;
|
||||
});
|
||||
|
||||
@@ -244,7 +260,7 @@ onMounted(async () => {
|
||||
<TableView :columns="columns" :model="filteredMailboxes" :busy="busy" :placeholder="$t(searchFilter ? 'email.incoming.mailboxes.noMatchesPlaceholder' : 'email.incoming.mailboxes.emptyPlaceholder')">
|
||||
<template #aliases="mailbox">{{ renderAliases(mailbox.aliases) }}</template>
|
||||
<template #usage="mailbox">
|
||||
<span v-if="mailbox.usage || mailbox.usage === 0">{{ prettyDecimalSize(mailbox.usage.diskSize) }}</span>
|
||||
<span v-if="mailbox.usage !== -1">{{ prettyDecimalSize(mailbox.usage.diskSize) }}</span>
|
||||
<span v-else>{{ $t('main.loadingPlaceholder') }} ...</span>
|
||||
</template>
|
||||
<template #storageQuota="mailbox">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { ref, reactive, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';
|
||||
import { Button, TextInput, MultiSelect } from '@cloudron/pankow';
|
||||
import { useDebouncedRef, copyToClipboard, prettyLongDate } from '@cloudron/pankow/utils';
|
||||
import { useDebouncedRef, copyToClipboard, prettyLongDate, prettyShortDate } from '@cloudron/pankow/utils';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import EventlogsModel from '../models/EventlogsModel.js';
|
||||
import { eventlogDetails, eventlogSource } from '../utils.js';
|
||||
@@ -15,72 +15,83 @@ function getApp(id) {
|
||||
}
|
||||
|
||||
const availableActions = [
|
||||
{ id: 'app.backup' },
|
||||
{ id: 'app.backup.finish' },
|
||||
{ id: 'app.configure' },
|
||||
{ id: 'app.install' },
|
||||
{ id: 'app.restore' },
|
||||
{ id: 'app.uninstall' },
|
||||
{ id: 'app.update' },
|
||||
{ id: 'app.update.finish' },
|
||||
{ id: 'app.login' },
|
||||
{ id: 'app.oom' },
|
||||
{ id: 'app.down' },
|
||||
{ id: 'app.up' },
|
||||
{ id: 'app.start' },
|
||||
{ id: 'app.stop' },
|
||||
{ id: 'app.restart' },
|
||||
{ id: 'backup.cleanup' },
|
||||
{ id: 'backup.cleanup.finish' },
|
||||
{ id: 'backup.finish' },
|
||||
{ id: 'backup.start' },
|
||||
{ id: 'backuptarget.add' },
|
||||
{ id: 'branding.avatar' },
|
||||
{ id: 'branding.footer' },
|
||||
{ id: 'branding.name' },
|
||||
{ id: 'certificate.new' },
|
||||
{ id: 'certificate.renew' },
|
||||
{ id: 'certificate.cleanup' },
|
||||
{ id: 'cloudron.activate' },
|
||||
{ id: 'cloudron.provision' },
|
||||
{ id: 'cloudron.restore' },
|
||||
{ id: 'cloudron.start' },
|
||||
{ id: 'cloudron.update' },
|
||||
{ id: 'cloudron.update.finish' },
|
||||
{ id: 'dashboard.domain.update' },
|
||||
{ id: 'directoryserver.configure' },
|
||||
{ id: 'dyndns.update' },
|
||||
{ id: 'domain.add' },
|
||||
{ id: 'domain.update' },
|
||||
{ id: 'domain.remove' },
|
||||
{ id: 'externalldap.configure' },
|
||||
{ id: 'group.add' },
|
||||
{ id: 'group.update' },
|
||||
{ id: 'group.remove' },
|
||||
{ id: 'mail.location' },
|
||||
{ id: 'mail.enabled' },
|
||||
{ id: 'mail.box.add' },
|
||||
{ id: 'mail.box.update' },
|
||||
{ id: 'mail.box.remove' },
|
||||
{ id: 'mail.list.add' },
|
||||
{ id: 'mail.list.update' },
|
||||
{ id: 'mail.list.remove' },
|
||||
{ id: 'service.configure' },
|
||||
{ id: 'service.rebuild' },
|
||||
{ id: 'service.restart' },
|
||||
{ id: 'support.ticket' },
|
||||
{ id: 'support.ssh' },
|
||||
{ id: 'user.add' },
|
||||
{ id: 'user.login' },
|
||||
{ id: 'user.login.ghost' },
|
||||
{ id: 'user.logout' },
|
||||
{ id: 'user.remove' },
|
||||
{ id: 'user.transfer' },
|
||||
{ id: 'user.update' },
|
||||
{ id: 'userdirectory.profileconfig.update' },
|
||||
{ id: 'volume.add' },
|
||||
{ id: 'volume.update' },
|
||||
{ id: 'volume.remove' },
|
||||
{ separator: true, label: 'App' },
|
||||
{ id: 'app.backup', label: 'Backup started' },
|
||||
{ id: 'app.backup.finish', label: 'Backup finished' },
|
||||
{ id: 'app.configure', label: 'Reconfigured' },
|
||||
{ id: 'app.install', label: 'Installed' },
|
||||
{ id: 'app.restore', label: 'Restored' },
|
||||
{ id: 'app.uninstall', label: 'Uninstalled' },
|
||||
{ id: 'app.update', label: 'Update started' },
|
||||
{ id: 'app.update.finish', label: 'Update finished' },
|
||||
{ id: 'app.login', label: 'Log in' },
|
||||
{ id: 'app.oom', label: 'Out of memory' },
|
||||
{ id: 'app.down', label: 'Down' },
|
||||
{ id: 'app.up', label: 'Up' },
|
||||
{ id: 'app.start', label: 'Started' },
|
||||
{ id: 'app.stop', label: 'Stopped' },
|
||||
{ id: 'app.restart', label: 'Restarted' },
|
||||
{ separator: true, label: 'Platform backup' },
|
||||
{ id: 'backup.cleanup', label: 'Cleanup started' },
|
||||
{ id: 'backup.cleanup.finish', label: 'Cleanup finished' },
|
||||
{ id: 'backup.start', label: 'Started' },
|
||||
{ id: 'backup.finish', label: 'Finished' },
|
||||
{ id: 'backuptarget.add', label: 'Site added' },
|
||||
{ separator: true, label: 'Certificates' },
|
||||
{ id: 'certificate.new', label: 'Obtained' },
|
||||
{ id: 'certificate.renew', label: 'Renewed' },
|
||||
{ id: 'certificate.cleanup', label: 'Cleaned up' },
|
||||
{ separator: true, label: 'Domains' },
|
||||
{ id: 'domain.add', label: 'Added' },
|
||||
{ id: 'domain.update', label: 'Updated' },
|
||||
{ id: 'domain.remove', label: 'Removed' },
|
||||
{ separator: true, label: 'Email' },
|
||||
{ id: 'mail.location', label: 'Location changed' },
|
||||
{ id: 'mail.enabled', label: 'Enabled/Disabled' },
|
||||
{ id: 'mail.box.add', label: 'Mailbox added' },
|
||||
{ id: 'mail.box.update', label: 'Mailbox updated' },
|
||||
{ id: 'mail.box.remove', label: 'Mailbox removed' },
|
||||
{ id: 'mail.list.add', label: 'Mailinglist added' },
|
||||
{ id: 'mail.list.update', label: 'Mailinglist updated' },
|
||||
{ id: 'mail.list.remove', label: 'Mailinglist removed' },
|
||||
{ separator: true, label: 'Services' },
|
||||
{ id: 'service.configure', label: 'Configured' },
|
||||
{ id: 'service.rebuild', label: 'Rebuilt' },
|
||||
{ id: 'service.restart', label: 'Restarted' },
|
||||
{ separator: true, label: 'Users' },
|
||||
{ id: 'user.add', label: 'Added' },
|
||||
{ id: 'user.update', label: 'Updated' },
|
||||
{ id: 'user.remove', label: 'Removed' },
|
||||
{ id: 'user.login', label: 'Logged in' },
|
||||
{ id: 'user.login.ghost', label: 'Ghost logged in' },
|
||||
{ id: 'user.logout', label: 'Logged out' },
|
||||
// { id: 'user.transfer', label: 'Transferred' },
|
||||
{ separator: true, label: 'Groups' },
|
||||
{ id: 'group.add', label: 'Added' },
|
||||
{ id: 'group.update', label: 'Updated' },
|
||||
{ id: 'group.remove', label: 'Removed' },
|
||||
{ separator: true, label: 'Volumes' },
|
||||
{ id: 'volume.add', label: 'Added' },
|
||||
{ id: 'volume.update', label: 'Updated' },
|
||||
{ id: 'volume.remove', label: 'Removed' },
|
||||
{ separator: true, label: 'Branding' },
|
||||
{ id: 'branding.avatar', label: 'Avatar changed' },
|
||||
{ id: 'branding.footer', label: 'Footer changed' },
|
||||
{ id: 'branding.name', label: 'Name started' },
|
||||
{ separator: true, label: 'Cloudron' },
|
||||
{ id: 'cloudron.activate', label: 'Activated' },
|
||||
{ id: 'cloudron.provision', label: 'Provisioned' },
|
||||
{ id: 'cloudron.restore', label: 'Restored' },
|
||||
{ id: 'cloudron.start', label: 'Started' },
|
||||
{ id: 'cloudron.update', label: 'Update started' },
|
||||
{ id: 'cloudron.update.finish', label: 'Update finished' },
|
||||
{ id: 'dashboard.domain.update', label: 'Dashboard domain updated' },
|
||||
{ id: 'dyndns.update', label: 'DynDNS changed' },
|
||||
{ id: 'directoryserver.configure', label: 'LDAP configured ' },
|
||||
{ id: 'externalldap.configure', label: 'External LDAP configured' },
|
||||
{ id: 'userdirectory.profileconfig.update', label: 'Profile config changed' },
|
||||
// { id: 'support.ssh', label: '' },
|
||||
// { id: 'support.ticket', label: '' },
|
||||
];
|
||||
|
||||
const refreshBusy = ref(false);
|
||||
@@ -90,6 +101,7 @@ const search = useDebouncedRef('');
|
||||
const page = ref(1);
|
||||
const perPage = ref(40);
|
||||
const actions = reactive([]);
|
||||
const eventlogContainer = useTemplateRef('eventlogContainer');
|
||||
|
||||
async function onRefresh() {
|
||||
refreshBusy.value = true;
|
||||
@@ -102,29 +114,31 @@ async function onRefresh() {
|
||||
return {
|
||||
id: Symbol(),
|
||||
raw: e,
|
||||
details: eventlogDetails(e, e.appId ? getApp(e.appId) : null),
|
||||
source: eventlogSource(e, e.appId ? getApp(e.appId) : null)
|
||||
details: eventlogDetails(e, e.data?.appId ? getApp(e.data.appId) : null),
|
||||
source: eventlogSource(e, e.data?.appId ? getApp(e.data.appId) : null)
|
||||
};
|
||||
});
|
||||
|
||||
refreshBusy.value = false;
|
||||
}
|
||||
|
||||
async function onScroll(event) {
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
|
||||
page.value++;
|
||||
const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value);
|
||||
if (error) return console.error(error);
|
||||
async function fetchMore() {
|
||||
page.value++;
|
||||
const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
eventlogs.value = eventlogs.value.concat(result.map(e => {
|
||||
return {
|
||||
id: Symbol(),
|
||||
raw: e,
|
||||
details: eventlogDetails(e, e.appId ? getApp(e.appId) : null),
|
||||
source: eventlogSource(e, e.appId ? getApp(e.appId) : null)
|
||||
};
|
||||
}));
|
||||
}
|
||||
eventlogs.value = eventlogs.value.concat(result.map(e => {
|
||||
return {
|
||||
id: Symbol(),
|
||||
raw: e,
|
||||
details: eventlogDetails(e, e.appId ? getApp(e.appId) : null),
|
||||
source: eventlogSource(e, e.appId ? getApp(e.appId) : null)
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
async function onScroll(event) {
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) await fetchMore();
|
||||
}
|
||||
|
||||
function onCopySource(eventlog) {
|
||||
@@ -147,7 +161,13 @@ onMounted(async () => {
|
||||
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
onHashChange();
|
||||
if (!search.value) onRefresh();
|
||||
if (!search.value) {
|
||||
onRefresh();
|
||||
|
||||
while (eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) {
|
||||
await fetchMore();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -164,37 +184,24 @@ onUnmounted(() => {
|
||||
{{ $t('eventlog.title') }}
|
||||
<div>
|
||||
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="search"/>
|
||||
<MultiSelect :search-threshold="10" v-model="actions" :options="availableActions" option-label="id" option-key="id" :selected-label="actions.length ? $t('main.multiselect.selected', { n: actions.length }) : $t('eventlog.filterAllEvents')"/>
|
||||
<MultiSelect :search-threshold="10" v-model="actions" :options="availableActions" option-label="label" option-key="id" :selected-label="actions.length ? $t('main.multiselect.selected', { n: actions.length }) : $t('eventlog.filterAllEvents')"/>
|
||||
<Button tool secondary @click="onRefresh()" :loading="refreshBusy" icon="fa-solid fa-sync-alt" />
|
||||
</div>
|
||||
</h1>
|
||||
<div class="section-body" style="overflow: auto; margin-top: 10px; padding-top: 0px" @scroll="onScroll">
|
||||
<table class="eventlog-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 160px">{{ $t('eventlog.time') }}</th>
|
||||
<th style="width: 100px">{{ $t('eventlog.source') }}</th>
|
||||
<th>{{ $t('eventlog.details') }}</th>
|
||||
<th style="width: 50px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="eventlog in eventlogs" :key="eventlog.id">
|
||||
<tr @click="eventlog.isOpen = !eventlog.isOpen" :class="{ 'active': eventlog.isOpen }" >
|
||||
<td style="white-space: nowrap;">{{ prettyLongDate(eventlog.raw.creationTime) }}</td>
|
||||
<td>{{ eventlog.source }}</td>
|
||||
<td class="elide-table-cell" v-html="eventlog.details"></td>
|
||||
<td><Button v-if="eventlog.raw.data.taskId" @click.stop plain small tool :href="`/logs.html?taskId=${eventlog.raw.data.taskId}`" target="_blank">Logs</Button></td>
|
||||
</tr>
|
||||
<tr v-show="eventlog.isOpen">
|
||||
<td colspan="4" class="eventlog-details">
|
||||
<div v-if="eventlog.raw.source.ip" class="eventlog-source">Source IP: <span @click="onCopySource(eventlog)">{{ eventlog.raw.source.ip }}</span></div>
|
||||
<pre>{{ JSON.stringify(eventlog.raw.data, null, 4) }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-body" ref="eventlogContainer" style="overflow: auto; margin-top: 10px; padding-top: 0px" @scroll="onScroll">
|
||||
<div class="eventlog-item" v-for="eventlog in eventlogs" :key="eventlog.id" :class="{ 'active': eventlog.isOpen }" @click="eventlog.isOpen = !eventlog.isOpen">
|
||||
<div class="eventlog-summary">
|
||||
<div style="width: 160px; flex-shrink: 0;" class="pankow-no-mobile">{{ prettyLongDate(eventlog.raw.creationTime) }}</div>
|
||||
<div style="width: 80px; flex-shrink: 0;" class="pankow-no-desktop">{{ prettyShortDate(eventlog.raw.creationTime) }}</div>
|
||||
<div style="width: 160px; flex-shrink: 0; font-weight: bold; overflow: hidden; text-overflow: ellipsis;" class="pankow-no-mobile">{{ eventlog.source }}</div>
|
||||
<div style="flex-grow: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" v-html="eventlog.details"></div>
|
||||
<!-- <div style="width: 160px; flex-shrink: 0; cursor: copy; overflow: hidden; text-overflow: ellipsis;" v-if="eventlog.raw.source.ip" @click="onCopySource(eventlog)">{{ eventlog.raw.source.ip }}</div> -->
|
||||
<div style="width: 40px; flex-shrink: 0;"><Button v-if="eventlog.raw.data.taskId" @click.stop plain small tool :href="`/logs.html?taskId=${eventlog.raw.data.taskId}`" target="_blank">Logs</Button></div>
|
||||
</div>
|
||||
<div v-show="eventlog.isOpen" class="eventlog-details" @click.stop>
|
||||
<pre>{{ JSON.stringify(eventlog.raw.data, null, 4) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,5 +209,28 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
.eventlog-item {
|
||||
border-radius: var(--pankow-border-radius);
|
||||
cursor: pointer;
|
||||
/*padding: 5px 10px;*/
|
||||
}
|
||||
|
||||
.eventlog-item.active,
|
||||
.eventlog-item:hover {
|
||||
background-color: var(--pankow-color-background-hover);
|
||||
}
|
||||
|
||||
.eventlog-summary {
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.eventlog-details {
|
||||
background-color: color-mix(in oklab, var(--pankow-color-background-hover), black 5%);
|
||||
cursor: auto;
|
||||
position: relative;
|
||||
border-radius: var(--pankow-border-radius);
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -209,7 +209,7 @@ onMounted(async () => {
|
||||
|
||||
<Section :title="$t('profile.title')">
|
||||
<template #header-buttons>
|
||||
<Button @click="profileModel.logout()">{{ $t('main.logout') }}</Button>
|
||||
<Button secondary @click="profileModel.logout()">{{ $t('main.logout') }}</Button>
|
||||
</template>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 20px">
|
||||
|
||||
@@ -13,7 +13,6 @@ const loginUrl = window.cloudron.loginUrl;
|
||||
<div class="main">
|
||||
<div>
|
||||
<img :src="iconUrl" /><br/>
|
||||
<small>{{ $t('login.loginTo') }}</small>
|
||||
<h1>{{ name }}</h1>
|
||||
<br/>
|
||||
<Button id="loginProceedButton" :href="loginUrl">{{ $t('login.loginAction') }}</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Notification, Button, SingleSelect, FormGroup, PasswordInput, TextInput, Checkbox } from '@cloudron/pankow';
|
||||
import { copyToClipboard } from '@cloudron/pankow/utils';
|
||||
import { REGIONS_CONTABO, REGIONS_VULTR, REGIONS_IONOS, REGIONS_OVH, REGIONS_LINODE, REGIONS_SCALEWAY, REGIONS_WASABI } from '../constants.js';
|
||||
import { redirectIfNeeded, mountlike, s3like } from '../utils.js';
|
||||
import { redirectIfNeeded, mountlike, s3like, parseFullBackupPath } from '../utils.js';
|
||||
import ProvisionModel from '../models/ProvisionModel.js';
|
||||
import BackupProviderForm from '../components/BackupProviderForm.vue';
|
||||
import Whirlpool from '../components/Whirlpool.vue';
|
||||
@@ -27,7 +27,7 @@ const progressMessage = ref('');
|
||||
const taskMinutesActive = ref(0);
|
||||
const provider = ref('');
|
||||
const providerConfig = ref({});
|
||||
const remotePath = ref('');
|
||||
const fullPath = ref('');
|
||||
const format = ref('');
|
||||
const encrypted = ref(false);
|
||||
const encryptionPasswordHint = ref('');
|
||||
@@ -87,21 +87,21 @@ async function onSubmit() {
|
||||
busy.value = true;
|
||||
formError.value = {};
|
||||
|
||||
if (remotePath.value.indexOf('/') === -1) {
|
||||
if (fullPath.value.indexOf('/') === -1) {
|
||||
error.value.generic = 'Backup id must include the directory path';
|
||||
error.value.remotePath = true;
|
||||
busy.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (remotePath.value.indexOf('box') === -1) {
|
||||
if (fullPath.value.indexOf('box') === -1) {
|
||||
error.value.generic = 'Backup id must contain "box"';
|
||||
error.value.remotePath = true;
|
||||
busy.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const version = remotePath.value.match(/_v(\d+.\d+.\d+)/);
|
||||
const version = fullPath.value.match(/_v(\d+.\d+.\d+)/);
|
||||
if (!version) {
|
||||
formError.value.generic = 'Backup id is missing version information';
|
||||
formError.value.remotePath = true;
|
||||
@@ -109,38 +109,12 @@ async function onSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {}; // filled below
|
||||
|
||||
const data = {
|
||||
backupConfig: {
|
||||
provider: provider.value,
|
||||
config, // filled below
|
||||
format: format.value,
|
||||
},
|
||||
remotePath: remotePath.value,
|
||||
version: version ? version[1] : '',
|
||||
ipv4Config: {
|
||||
provider: ipv4Provider.value,
|
||||
ip: ipv4Address.value,
|
||||
ifname: ipv4Interface.value,
|
||||
},
|
||||
ipv6Config: {
|
||||
provider: ipv6Provider.value,
|
||||
ip: ipv6Address.value,
|
||||
ifname: ipv6Interface.value,
|
||||
},
|
||||
skipDnsSetup: skipDnsSetup.value,
|
||||
siteId: siteId.value
|
||||
};
|
||||
|
||||
if (encrypted.value) {
|
||||
data.backupConfig.encryptionPassword = encryptionPassword.value;
|
||||
data.backupConfig.encryptedFilenames = encryptedFilenames.value;
|
||||
}
|
||||
const config = {};
|
||||
const { prefix, remotePath } = parseFullBackupPath(fullPath.value);
|
||||
|
||||
if (s3like(provider.value)) {
|
||||
config.endpoint = providerConfig.value.endpoint;
|
||||
config.prefix = providerConfig.value.prefix;
|
||||
config.prefix = prefix;
|
||||
config.bucket = providerConfig.value.bucket;
|
||||
config.accessKeyId = providerConfig.value.accessKeyId;
|
||||
config.secretAccessKey = providerConfig.value.secretAccessKey;
|
||||
@@ -187,7 +161,7 @@ async function onSubmit() {
|
||||
config.signatureVersion = 'v4';
|
||||
}
|
||||
} else if (mountlike(provider.value)) {
|
||||
config.prefix = providerConfig.value.prefix;
|
||||
config.prefix = prefix;
|
||||
config.noHardlinks = !providerConfig.value.useHardlinks;
|
||||
config.mountOptions = {};
|
||||
|
||||
@@ -215,16 +189,43 @@ async function onSubmit() {
|
||||
config.preserveAttributes = !!providerConfig.value.preserveAttributes;
|
||||
}
|
||||
} else if (provider.value === 'filesystem') {
|
||||
config.backupDir = providerConfig.value.backupDir;
|
||||
config.backupDir = prefix;
|
||||
config.noHardlinks = !providerConfig.value.useHardlinks;
|
||||
config.preserveAttributes = true;
|
||||
} else if (provider.value === 'gcs') {
|
||||
config.bucket = providerConfig.value.bucket;
|
||||
config.prefix = providerConfig.value.prefix;
|
||||
config.prefix = prefix;
|
||||
config.projectId = providerConfig.value.projectId;
|
||||
config.credentials = providerConfig.value.credentials;
|
||||
}
|
||||
|
||||
const data = {
|
||||
backupConfig: {
|
||||
provider: provider.value,
|
||||
config,
|
||||
format: format.value,
|
||||
},
|
||||
remotePath,
|
||||
version: version ? version[1] : '',
|
||||
ipv4Config: {
|
||||
provider: ipv4Provider.value,
|
||||
ip: ipv4Address.value,
|
||||
ifname: ipv4Interface.value,
|
||||
},
|
||||
ipv6Config: {
|
||||
provider: ipv6Provider.value,
|
||||
ip: ipv6Address.value,
|
||||
ifname: ipv6Interface.value,
|
||||
},
|
||||
skipDnsSetup: skipDnsSetup.value,
|
||||
siteId: siteId.value
|
||||
};
|
||||
|
||||
if (encrypted.value) {
|
||||
data.backupConfig.encryptionPassword = encryptionPassword.value;
|
||||
data.backupConfig.encryptedFilenames = encryptedFilenames.value;
|
||||
}
|
||||
|
||||
const [error] = await provisionModel.restore(data);
|
||||
if (error) {
|
||||
if (error.status === 424) {
|
||||
@@ -274,14 +275,36 @@ function onBackupConfigChanged(event) {
|
||||
}
|
||||
|
||||
provider.value = data.provider;
|
||||
remotePath.value = data.remotePath;
|
||||
providerConfig.value = data.config;
|
||||
fullPath.value = data.config.prefix ? `${data.config.prefix}/${data.remotePath}` : data.remotePath;
|
||||
format.value = data.format;
|
||||
encrypted.value = !!data.encrypted;
|
||||
encryptionPasswordHint.value = data.encryptionPasswordHint || '';
|
||||
encryptionPassword.value = '';
|
||||
encryptedFilenames.value = data.encryptedFilenames;
|
||||
siteId.value = data.siteId || '';
|
||||
|
||||
providerConfig.value = {};
|
||||
for (const [key, value] of Object.entries(data.config)) {
|
||||
if (key === 'noHardlinks' || key === 'chown' || key === 'preserveAttributes') {
|
||||
// not really used for restoring
|
||||
} else if (key === 'mountOptions') { // providerConfig uses a flattened format of config.mountOptions
|
||||
providerConfig.value.mountOptionHost = data.config.mountOptions.host;
|
||||
providerConfig.value.mountOptionPort = data.config.mountOptions.port;
|
||||
providerConfig.value.mountOptionRemoteDir = data.config.mountOptions.remoteDir;
|
||||
providerConfig.value.mountOptionSeal = !!data.config.mountOptions.seal;
|
||||
providerConfig.value.mountOptionDiskPath = data.config.mountOptions.diskPath;
|
||||
providerConfig.value.mountOptionUser = data.config.mountOptions.user;
|
||||
providerConfig.value.mountOptionUsername = data.config.mountOptions.username;
|
||||
providerConfig.value.mountOptionPassword = data.config.mountOptions.password;
|
||||
providerConfig.value.mountOptionPrivateKey = '';
|
||||
} else {
|
||||
// s3: 'accessKeyId', 'secretAccessKey', 'bucket', 'prefix', 'signatureVersion', 'endpoint', 'region', 'acceptSelfSignedCerts', 's3ForcePathStyle'
|
||||
// gcs: 'bucket', 'prefix'
|
||||
providerConfig.value[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(checkValidity, 100); // update state of the confirm button
|
||||
};
|
||||
|
||||
reader.readAsText(event.target.files[0]);
|
||||
@@ -351,7 +374,7 @@ onMounted(async () => {
|
||||
<!-- remotePath contains the prefix as well -->
|
||||
<FormGroup>
|
||||
<label for="inputRemotePath">{{ $t('app.importBackupDialog.remotePath') }} <sup><a href="https://docs.cloudron.io/backups/#import-app-backup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<TextInput id="inputRemotePath" v-model="remotePath" required />
|
||||
<TextInput id="inputRemotePath" v-model="fullPath" required />
|
||||
</FormGroup>
|
||||
|
||||
<BackupProviderForm ref="form"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { computed, reactive, onMounted, ref, useTemplateRef, nextTick } from 'vue';
|
||||
import { computed, reactive, onMounted, ref, useTemplateRef, nextTick, onUnmounted } from 'vue';
|
||||
import { Button, Menu, TableView, ProgressBar, FormGroup, Checkbox, Dialog } from '@cloudron/pankow';
|
||||
import { prettyBinarySize } from '@cloudron/pankow/utils';
|
||||
import { each } from 'async';
|
||||
@@ -39,27 +39,27 @@ const columns = {
|
||||
|
||||
const actionMenuModel = ref([]);
|
||||
const actionMenuElement = useTemplateRef('actionMenuElement');
|
||||
function onActionMenu(service, event) {
|
||||
function onActionMenu(id, event) {
|
||||
actionMenuModel.value = [{
|
||||
icon: 'fa-solid fa-pencil-alt',
|
||||
label: t('main.action.configure'),
|
||||
disabled() { return refreshBusy.value; },
|
||||
visible: service.status !== 'disabled' && service.memoryLimit,
|
||||
action: onEdit.bind(null, service),
|
||||
visible: services[id].status !== 'disabled' && services[id].memoryLimit,
|
||||
action: onEdit.bind(null, id),
|
||||
}, {
|
||||
separator: true,
|
||||
visible: service.status !== 'disabled' && service.memoryLimit,
|
||||
visible: services[id].status !== 'disabled' && services[id].memoryLimit,
|
||||
}, {
|
||||
icon: 'fa-solid fa-sync-alt',
|
||||
label: t('services.restartActionTooltip'),
|
||||
visible: service.id !== 'box',
|
||||
disabled: (service.status === 'starting' && !service.config.recoveryMode),
|
||||
action: onRestart.bind(null, service.id),
|
||||
visible: id !== 'box' && services[id].status !== 'disabled',
|
||||
disabled: services[id].status === 'starting' && services[id].config.recoveryMode,
|
||||
action: onRestart.bind(null, id),
|
||||
}, {
|
||||
icon: 'fa-solid fa-fw fa-file-alt',
|
||||
label: t('logs.title'),
|
||||
target: '_blank',
|
||||
href: `/logs.html?id=${service.id}`,
|
||||
href: `/logs.html?id=${id}`,
|
||||
}];
|
||||
|
||||
actionMenuElement.value.open(event, event.currentTarget);
|
||||
@@ -83,8 +83,14 @@ const servicesArray = computed(() => {
|
||||
});
|
||||
|
||||
let apps = [];
|
||||
const servicesTimers = {};
|
||||
|
||||
async function refresh(id) {
|
||||
if (servicesTimers[id]) {
|
||||
clearTimeout(servicesTimers[id]);
|
||||
delete servicesTimers[id];
|
||||
}
|
||||
|
||||
const [error, result] = await servicesModel.get(id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
@@ -97,7 +103,7 @@ async function refresh(id) {
|
||||
services[id].defaultMemoryLimit = result.defaultMemoryLimit;
|
||||
|
||||
// we will poll until active
|
||||
if (result.status !== 'active') setTimeout(refresh.bind(null, id), 3000);
|
||||
if (result.status !== 'active' && !result.config.recoveryMode) servicesTimers[id] = setTimeout(refresh.bind(null, id), 3000);
|
||||
}
|
||||
|
||||
const refreshBusy = ref(false);
|
||||
@@ -147,10 +153,10 @@ const editRecoveryMode = ref(false);
|
||||
|
||||
let availableSystemMemory = 4 * 1024 * 1024 * 1024;
|
||||
|
||||
async function onEdit(service) {
|
||||
editService.value = service;
|
||||
editMemoryLimit.value = service.config.memoryLimit;
|
||||
editRecoveryMode.value = service.config.recoveryMode;
|
||||
async function onEdit(id) {
|
||||
editService.value = services[id];
|
||||
editMemoryLimit.value = services[id].config.memoryLimit;
|
||||
editRecoveryMode.value = services[id].config.recoveryMode;
|
||||
|
||||
editMemoryTicks.value = [];
|
||||
// we max system memory and current service memory for the case where the user configured the service on another server with more resources
|
||||
@@ -189,19 +195,23 @@ async function onEditSubmit() {
|
||||
}
|
||||
|
||||
function state(service) {
|
||||
if (service.status === 'active') return 'success';
|
||||
else if (service.status === 'starting' && service.config.recoveryMode) return '';
|
||||
else if (service.status === 'starting') return 'warning';
|
||||
else return 'danger';
|
||||
switch (service.status) {
|
||||
case 'active': return 'success';
|
||||
case 'disabled': return '';
|
||||
case 'stopped': return 'danger';
|
||||
case 'starting': return service.config.recoveryMode ? '' : 'warning';
|
||||
default: return 'danger';
|
||||
}
|
||||
}
|
||||
|
||||
function stateTooltip(service) {
|
||||
if (!service.status) return '';
|
||||
|
||||
if (service.status === 'active') return 'Active';
|
||||
else if (service.status === 'starting' && service.config.recoveryMode) return 'Recovery mode';
|
||||
else if (service.status === 'starting') return 'Starting';
|
||||
else return service.status;
|
||||
switch (service.status) {
|
||||
case 'active': return 'Active';
|
||||
case 'disabled': return 'Disabled';
|
||||
case 'stopped': return 'Stopped';
|
||||
case 'starting': return service.config.recoveryMode ? 'Recovery mode' : 'Starting';
|
||||
default: return service.status;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -212,6 +222,10 @@ onMounted(async () => {
|
||||
availableSystemMemory = result.memory;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
Object.values(servicesTimers).forEach(clearTimeout);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -263,7 +277,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
<template #actions="service">
|
||||
<div style="text-align: right;">
|
||||
<Button tool plain secondary @click.capture="onActionMenu(service, $event)" icon="fa-solid fa-ellipsis" />
|
||||
<Button tool plain secondary @click.capture="onActionMenu(service.id, $event)" icon="fa-solid fa-ellipsis" />
|
||||
</div>
|
||||
</template>
|
||||
</TableView>
|
||||
|
||||
Generated
+1
-8
@@ -63,8 +63,7 @@
|
||||
"expect.js": "*",
|
||||
"mocha": "^11.7.5",
|
||||
"nock": "^14.0.10",
|
||||
"ssh2": "^1.17.0",
|
||||
"yesno": "^0.4.0"
|
||||
"ssh2": "^1.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -8683,12 +8682,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yesno": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz",
|
||||
"integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
|
||||
+1
-2
@@ -67,8 +67,7 @@
|
||||
"expect.js": "*",
|
||||
"mocha": "^11.7.5",
|
||||
"nock": "^14.0.10",
|
||||
"ssh2": "^1.17.0",
|
||||
"yesno": "^0.4.0"
|
||||
"ssh2": "^1.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./run-tests"
|
||||
|
||||
@@ -22,6 +22,9 @@ readonly HELP_MESSAGE="
|
||||
See https://docs.cloudron.io/troubleshooting for more information on troubleshooting.
|
||||
|
||||
Options:
|
||||
--apply-db-migrations Applies all pending DB migrations
|
||||
--check-db-migrations Checks if the DB migrations are up to date
|
||||
--check-services Checks if services/addons are running and healthy.
|
||||
--disable-dnssec Disable DNSSEC
|
||||
--enable-remote-support Enable SSH Remote Access for the Cloudron support team
|
||||
--disable-remote-support Disable SSH Remote Access for the Cloudron support team
|
||||
@@ -634,6 +637,8 @@ function troubleshoot() {
|
||||
check_nginx # requires mysql to be checked
|
||||
check_dashboard_cert
|
||||
check_dashboard_site_loopback # checks website via loopback
|
||||
check_db_migrations
|
||||
check_services
|
||||
check_box
|
||||
check_netplan
|
||||
check_dns
|
||||
@@ -844,9 +849,60 @@ function apply_patch() {
|
||||
echo "Patch applied"
|
||||
}
|
||||
|
||||
function check_db_migrations() {
|
||||
local -r last_migration_from_db="$(mysql -NB -uroot -ppassword -e "SELECT name FROM box.migrations ORDER BY run_on DESC LIMIT 1" 2>/dev/null).js"
|
||||
local -r last_migration_file="/$(ls --ignore schema.sql --ignore initial-schema.sql /home/yellowtent/box/migrations/ | sort | tail -1)"
|
||||
if [[ "${last_migration_from_db}" != "${last_migration_file}" ]]; then
|
||||
fail "Database migrations are pending. Last migration in DB: ${last_migration_from_db}. Last migration file: ${last_migration_file}."
|
||||
info "Please run 'cloudron-support --apply-db-migrations' to apply the migrations."
|
||||
else
|
||||
success "No pending database migrations"
|
||||
fi
|
||||
}
|
||||
|
||||
function apply_db_migrations() {
|
||||
echo "Applying pending database migrations"
|
||||
bash /home/yellowtent/box/setup/start.sh && success "Database migrations applied successfully" || fail "Database migrations failed"
|
||||
}
|
||||
|
||||
function check_services() {
|
||||
local services=("mysql" "postgresql" "mongodb" "mail" "graphite")
|
||||
local service_ip=("172.18.30.1" "172.18.30.2" "172.18.30.3" "172.18.30.4" "172.18.30.5")
|
||||
local service_port=("3000" "3000" "3000" "3000" "2003")
|
||||
|
||||
for service in "${!services[@]}"; do
|
||||
# Check if container is running
|
||||
if [[ $(docker inspect ${services[$service]} --format={{.State.Status}}) != "running" ]]; then
|
||||
fail "Service '${services[$service]}' container is not running!"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if service is reachable with simple nc check
|
||||
if ! nc -z -w5 ${service_ip[$service]} ${service_port[$service]} 2>/dev/null; then
|
||||
fail "Service '${services[$service]}' is not reachable"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Curl the healthcheck endpoint
|
||||
if [[ ${services[$service]} != "graphite" ]]; then
|
||||
if ! grep -q "true" <<< $(curl --fail -s "http://${service_ip[$service]}:${service_port[$service]}/healthcheck"); then
|
||||
fail "Service '${services[$service]}' healthcheck failed"
|
||||
continue
|
||||
fi
|
||||
else
|
||||
# Graphite has a different healthcheck endpoint and needs to be checked differently
|
||||
if ! grep -q "Graphite Dashboard" <<< "$(curl --fail -s http://${service_ip[$service]}:8000/graphite-web/dashboard)"; then
|
||||
fail "Service '${services[$service]}' healthcheck failed"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
success "Service '${services[$service]}' is running and healthy"
|
||||
done
|
||||
}
|
||||
|
||||
check_disk_space
|
||||
|
||||
args=$(getopt -o "" -l "admin-login,disable-dnssec,enable-remote-support,disable-remote-support,help,owner-login,patch:,recreate-containers,recreate-docker,fix-docker-version,send-diagnostics,unbound-use-external-dns,troubleshoot" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "admin-login,disable-dnssec,enable-remote-support,disable-remote-support,help,owner-login,patch:,recreate-containers,recreate-docker,fix-docker-version,send-diagnostics,unbound-use-external-dns,troubleshoot,check-db-migrations,apply-db-migrations,check-services" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
@@ -867,6 +923,9 @@ while true; do
|
||||
--recreate-containers) recreate_containers; exit 0;;
|
||||
--recreate-docker) recreate_docker; exit 0;;
|
||||
--fix-docker-version) fix_docker_version; exit 0;;
|
||||
--check-db-migrations) check_db_migrations; exit 0;;
|
||||
--apply-db-migrations) apply_db_migrations; exit 0;;
|
||||
--check-services) check_services; exit 0;;
|
||||
--patch) apply_patch "$2"; exit 0;;
|
||||
--help) break;;
|
||||
--) break;;
|
||||
|
||||
+41
-1
@@ -15,9 +15,49 @@ const assert = require('assert'),
|
||||
Table = require('easy-table'),
|
||||
url = require('url'),
|
||||
util = require('util'),
|
||||
yesno = require('yesno'),
|
||||
readline = require('readline'),
|
||||
_ = require('../src/underscore.js');
|
||||
|
||||
// slightly simplified from https://github.com/tcql/node-yesno/blob/master/yesno.js
|
||||
async function yesno({ question, defaultValue, yesValues, noValues }) {
|
||||
const options = {
|
||||
yes: [ 'yes', 'y' ],
|
||||
no: [ 'no', 'n' ]
|
||||
};
|
||||
|
||||
const yValues = (yesValues || options.yes).map((v) => v.toLowerCase());
|
||||
const nValues = (noValues || options.no).map((v) => v.toLowerCase());
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
rl.question(question + ' ', async function (answer) {
|
||||
rl.close();
|
||||
|
||||
const cleaned = answer.trim().toLowerCase();
|
||||
|
||||
if (cleaned == '' && defaultValue != null)
|
||||
return resolve(defaultValue);
|
||||
|
||||
if (yValues.indexOf(cleaned) >= 0)
|
||||
return resolve(true);
|
||||
|
||||
if (nValues.indexOf(cleaned) >= 0)
|
||||
return resolve(false);
|
||||
|
||||
process.stdout.write('\nInvalid Response.\n');
|
||||
process.stdout.write('Answer either yes : (' + (yesValues || options.yes).join(', ') + ') \n');
|
||||
process.stdout.write('Or no: (' + (noValues || options.no).join(', ') + ') \n\n');
|
||||
|
||||
const result = await yesno({ question, defaultValue, yesValues, noValues });
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const ENVIRONMENTS = {
|
||||
'dev': {
|
||||
tag: 'dev',
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
/home/yellowtent/platformdata/logs/postgresql/*.log
|
||||
/home/yellowtent/platformdata/logs/sftp/*.log
|
||||
/home/yellowtent/platformdata/logs/redis-*/*.log
|
||||
/home/yellowtent/platformdata/logs/collectd/*.log
|
||||
/home/yellowtent/platformdata/logs/turn/*.log
|
||||
/home/yellowtent/platformdata/logs/updater/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
|
||||
@@ -22,7 +22,13 @@ http {
|
||||
# required for long host names
|
||||
server_names_hash_bucket_size 128;
|
||||
|
||||
access_log /var/log/nginx/access.log combined;
|
||||
# no query parameters since tokens might get logged
|
||||
log_format no_query '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request_method $uri $server_protocol" '
|
||||
'$status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent"';
|
||||
|
||||
access_log /var/log/nginx/access.log no_query;
|
||||
|
||||
sendfile on;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ ExecStart=/home/yellowtent/box/box.js
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
; we run commands like df which will parse properly only with correct locale
|
||||
; add "oidc-provider:*" to DEBUG for OpenID debugging
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,-box:ldap,-box:oidcserver" "BOX_ENV=cloudron" "NODE_ENV=production" "LC_ALL=C"
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,-box:ldapserver,-box:directoryserver,-box:oidcserver" "BOX_ENV=cloudron" "NODE_ENV=production" "LC_ALL=C"
|
||||
; this sends the main process SIGTERM and then if anything lingers to the control-group . this is also the case if the main process crashes.
|
||||
; the box code handles SIGTERM and cleanups the tasks
|
||||
KillMode=mixed
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[Unit]
|
||||
Description=Cloudron FS Resizer
|
||||
Before=docker.service collectd.service mysql.service sshd.service nginx.service
|
||||
Before=docker.service mysql.service sshd.service nginx.service
|
||||
After=cloud-init.service
|
||||
|
||||
[Service]
|
||||
|
||||
+6
-5
@@ -2860,11 +2860,12 @@ async function restoreApps(apps, options, auditSource) {
|
||||
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup
|
||||
|
||||
for (const app of apps) {
|
||||
const [error, result] = await safe(backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1));
|
||||
const [error, results] = await safe(backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1));
|
||||
let installationState, restoreConfig;
|
||||
if (!error && result.length) {
|
||||
if (!error && results.length) {
|
||||
installationState = exports.ISTATE_PENDING_RESTORE;
|
||||
restoreConfig = { remotePath: result[0].remotePath, backupSite: options.backupSite };
|
||||
// intentionally ignore options.backupSite since the site may not have all the apps
|
||||
restoreConfig = { backupId: results[0].id };
|
||||
} else {
|
||||
installationState = exports.ISTATE_PENDING_INSTALL;
|
||||
restoreConfig = null;
|
||||
@@ -2877,11 +2878,11 @@ async function restoreApps(apps, options, auditSource) {
|
||||
requireNullTaskId: false // ignore existing stale taskId
|
||||
};
|
||||
|
||||
debug(`restoreApps: marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
|
||||
debug(`restoreApps: marking ${app.fqdn} as ${installationState} using restore config ${JSON.stringify(restoreConfig)}`);
|
||||
|
||||
const [addTaskError, taskId] = await safe(addTask(app.id, installationState, task, auditSource));
|
||||
if (addTaskError) debug(`restoreApps: error marking ${app.fqdn} for restore: ${JSON.stringify(addTaskError)}`);
|
||||
else debug(`restoreApps: marked ${app.id} for restore with taskId ${taskId}`);
|
||||
else debug(`restoreApps: marked ${app.id} as ${installationState} with taskId ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,8 @@ async function saveFsMetadata(dataLayout, metadataFile) {
|
||||
|
||||
// we assume small number of files. spawnSync will raise a ENOBUFS error after maxBuffer
|
||||
for (const lp of dataLayout.localPaths()) {
|
||||
const [emptyDirsError, emptyDirs] = await safe(shell.spawn('find', [lp, '-type', 'd', '-empty'], { encoding: 'utf8', maxLines: 50000 }));
|
||||
if (emptyDirsError && emptyDirsError.stdoutLineCount >= 50000) throw new BoxError(BoxError.FS_ERROR, `Too many empty directories. Run "find ${lp} -type d -empty" to investigate`);
|
||||
const [emptyDirsError, emptyDirs] = await safe(shell.spawn('find', [lp, '-type', 'd', '-empty'], { encoding: 'utf8', maxLines: 80000 }));
|
||||
if (emptyDirsError && emptyDirsError.stdoutLineCount >= 80000) throw new BoxError(BoxError.FS_ERROR, `Too many empty directories. Run "find ${lp} -type d -empty" to investigate`);
|
||||
if (emptyDirsError) throw emptyDirsError;
|
||||
if (emptyDirs.length) metadata.emptyDirs = metadata.emptyDirs.concat(emptyDirs.trim().split('\n').map((ed) => dataLayout.toRemotePath(ed)));
|
||||
|
||||
|
||||
@@ -591,5 +591,8 @@ async function createPseudo(data) {
|
||||
async function reinitAll() {
|
||||
for (const site of await list()) {
|
||||
if (!safe.fs.mkdirSync(`${paths.BACKUP_INFO_DIR}/${site.id}`, { recursive: true })) throw new BoxError(BoxError.FS_ERROR, `Failed to create info dir: ${safe.error.message}`);
|
||||
const status = await getStatus(site);
|
||||
if (status.state === 'active') continue;
|
||||
safe(remount(site), { debug }); // background
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -420,7 +420,8 @@ async function syncGroups(config, progressCallback) {
|
||||
|
||||
const ldapGroups = await ldapGroupSearch(config, {});
|
||||
|
||||
debug(`syncGroups: Found ${ldapGroups.length} groups`);
|
||||
debug(`syncGroups: Found ${ldapGroups.length} groups:`);
|
||||
debug(ldapGroups);
|
||||
|
||||
let percent = 40;
|
||||
const step = 30/(ldapGroups.length+1); // ensure no divide by 0
|
||||
@@ -443,7 +444,7 @@ async function syncGroups(config, progressCallback) {
|
||||
if (error) debug('syncGroups: Failed to create group', groupName, error);
|
||||
} else {
|
||||
// convert local group to ldap group. 2 reasons:
|
||||
// 1. we reset source flag when externalldap is disabled. if we renable, it automatically coverts
|
||||
// 1. we reset source flag when externalldap is disabled. if we renable, it automatically converts
|
||||
// 2. externalldap connector usually implies user wants to user external users/groups.
|
||||
groups.update(result.id, { source: 'ldap' });
|
||||
debug(`syncGroups: [up-to-date group] groupname=${groupName}`);
|
||||
|
||||
@@ -13,7 +13,7 @@ exports = module.exports = {
|
||||
'images': {
|
||||
// 'base': 'registry.docker.com/cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c',
|
||||
'graphite': 'registry.docker.com/cloudron/graphite:3.5.1@sha256:5383f694245f25a386140268b490a41aa0ba6fb0024d92852546e40c8458681f',
|
||||
'mail': 'registry.docker.com/cloudron/mail:3.16.4@sha256:468239e1f7a9dc2cdf66750e66b83f1c561048fdd88ce7110fac89a5f7fb8777',
|
||||
'mail': 'registry.docker.com/cloudron/mail:3.17.0@sha256:64478f7f71b54fb2fb6ad09bc9bcc8bf6f6e2bd590f9c457e2a8ebdf927b373b',
|
||||
'mongodb': 'registry.docker.com/cloudron/mongodb:6.1.2@sha256:b35ad49ddb87f0bba449ecc438caa2d6e6960d31b3d1b5de1119b045aae1ea94',
|
||||
'mysql': 'registry.docker.com/cloudron/mysql:3.5.2@sha256:5cf52069a5ffb126afcaf6cdf91dba7e2c719efe48669e46b616979ef825e25b',
|
||||
'postgresql': 'registry.docker.com/cloudron/postgresql:6.1.1@sha256:8ba6d13670b475b03bffaf4b809e48254aa8baa271a1d0e1ce8a2288a2a56f82',
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ const addonConfigs = require('./addonconfigs.js'),
|
||||
AuditSource = require('./auditsource.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
debug = require('debug')('box:ldapserver'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
groups = require('./groups.js'),
|
||||
ldap = require('ldapjs'),
|
||||
|
||||
+16
-7
@@ -212,22 +212,25 @@ async function listDomains() {
|
||||
return results;
|
||||
}
|
||||
|
||||
async function checkOutboundPort25() {
|
||||
return await new Promise((resolve) => {
|
||||
async function checkOutboundPort25(family) {
|
||||
const ip = family === 4 ? await network.getIPv4() : await network.getIPv6();
|
||||
if (ip === null) return; // ipv4/ipv6 is disabled
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const client = new net.Socket();
|
||||
client.setTimeout(5000);
|
||||
client.connect({ port: 25, host: constants.PORT25_CHECK_SERVER, family: 4 }); // family is 4 to keep it predictable
|
||||
client.connect({ port: 25, host: constants.PORT25_CHECK_SERVER, family }); // family is 4 to keep it predictable
|
||||
client.on('connect', function () {
|
||||
client.destroy(); // do not use end() because it still triggers timeout
|
||||
resolve({ status: 'passed', message: 'Port 25 (outbound) is unblocked' });
|
||||
resolve();
|
||||
});
|
||||
client.on('timeout', function () {
|
||||
client.destroy();
|
||||
resolve({ status: 'failed', message: `Connect to ${constants.PORT25_CHECK_SERVER} timed out. Check if port 25 (outbound) is blocked` });
|
||||
reject(new Error(`IPv${family} connect to ${constants.PORT25_CHECK_SERVER} timed out`));
|
||||
});
|
||||
client.on('error', function (error) {
|
||||
client.destroy();
|
||||
resolve({ status: 'failed', message: `Connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}. Check if port 25 (outbound) is blocked` });
|
||||
reject(new Error(`IPv${family} connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -236,7 +239,13 @@ async function checkSmtpRelay(relay) {
|
||||
assert.strictEqual(typeof relay, 'object');
|
||||
|
||||
if (relay.provider === 'noop') return { status: 'skipped', message: 'Outbound disabled' };
|
||||
if (relay.provider === 'cloudron-smtp') return await checkOutboundPort25();
|
||||
if (relay.provider === 'cloudron-smtp') {
|
||||
const results = await Promise.allSettled([ checkOutboundPort25(4), checkOutboundPort25(6) ]);
|
||||
if (results[0].status === 'fulfilled' && results[1].status === 'fulfilled') return { status: 'passed', message: 'Port 25 outbound is unblocked' };
|
||||
if (results[0].status === 'fulfilled') return { status: 'passed', message: 'IPv4 port 25 outbound is unblocked. IPv6 port 25 outbound is blocked and delivery to IPv6 only servers will fail' }; // ipv6 only servers are not really common
|
||||
if (results[1].status === 'fulfilled') return { status: 'failed', message: `IPv4 port 25 outbound is blocked: ${results[0].reason.message}` }; // only IPv6 worked
|
||||
return { status: 'failed', message: `Port 25 outbound is blocked. ${results[0].reason.message}` }; // both failed
|
||||
}
|
||||
|
||||
const options = {
|
||||
connectionTimeout: 5000,
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ async function sendToGraphite() {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const client = new net.Socket();
|
||||
client.connect(constants.GRAPHITE_PORT, '127.0.0.1', () => {
|
||||
client.connect(constants.GRAPHITE_PORT, constants.GRAPHITE_SERVICE_IPv4, () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
for (const metric of graphiteMetrics) {
|
||||
|
||||
+1
-1
@@ -467,7 +467,7 @@ async function interactionConfirm(req, res, next) {
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const auditSource = AuditSource.fromOidcRequest(req);
|
||||
|
||||
await eventlog.add(user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: user.id, user: users.removePrivateFields(user), appId: params.client_id });
|
||||
await eventlog.add(user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: user.id, user: users.removePrivateFields(user), appId: client.appId || null });
|
||||
await users.notifyLoginLocation(user, ip, userAgent, auditSource);
|
||||
|
||||
const result = { consent };
|
||||
|
||||
@@ -31,8 +31,6 @@ elif [[ "${service}" == "nginx" ]]; then
|
||||
fi
|
||||
elif [[ "${service}" == "docker" ]]; then
|
||||
systemctl restart --no-block docker
|
||||
elif [[ "${service}" == "collectd" ]]; then
|
||||
systemctl restart --no-block collectd
|
||||
elif [[ "${service}" == "box" ]]; then
|
||||
systemctl reload --no-block box
|
||||
else
|
||||
|
||||
@@ -1781,7 +1781,6 @@ async function startGraphite(existingInfra) {
|
||||
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
|
||||
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
|
||||
|
||||
// port 2003 is used by collectd
|
||||
const runCmd = `docker run --restart=unless-stopped -d --name=graphite \
|
||||
--hostname graphite \
|
||||
--net cloudron \
|
||||
@@ -1793,7 +1792,6 @@ async function startGraphite(existingInfra) {
|
||||
-m ${memoryLimit} \
|
||||
--memory-swap -1 \
|
||||
--ip ${constants.GRAPHITE_SERVICE_IPv4} \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-v ${paths.PLATFORM_DATA_DIR}/graphite:/var/lib/graphite \
|
||||
--label isCloudronManaged=true \
|
||||
${readOnly} -v /tmp -v /run ${image} ${cmd}`;
|
||||
@@ -1808,9 +1806,6 @@ async function startGraphite(existingInfra) {
|
||||
await shell.bash(runCmd, { encoding: 'utf8' });
|
||||
|
||||
if (existingInfra.version !== 'none' && existingInfra.images.graphite !== image) await docker.deleteImage(existingInfra.images.graphite);
|
||||
|
||||
// restart collectd to get the disk stats after graphite starts. currently, there is no way to do graphite health check
|
||||
setTimeout(async () => await safe(shell.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {})), 60000);
|
||||
}
|
||||
|
||||
async function setupProxyAuth(app, options) {
|
||||
@@ -2123,11 +2118,6 @@ async function statusGraphite() {
|
||||
|
||||
async function restartGraphite() {
|
||||
await docker.restartContainer('graphite');
|
||||
|
||||
setTimeout(async () => {
|
||||
const [error] = await safe(shell.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {}));
|
||||
if (error) debug(`restartGraphite: error restarting collected. ${error.message}`);
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
async function teardownOauth(app, options) {
|
||||
|
||||
@@ -214,7 +214,7 @@ async function copyInternal(config, fromPath, toPath, options, progressCallback)
|
||||
if (!safe.fs.writeFileSync(identityFilePath, `${config.mountOptions.privateKey}\n`, { mode: 0o600 })) throw new BoxError(BoxError.FS_ERROR, `Could not write temporary private key: ${safe.error.message}`);
|
||||
|
||||
const sshOptions = [ '-o', '"StrictHostKeyChecking no"', '-i', identityFilePath, '-p', config.mountOptions.port, `${config.mountOptions.user}@${config.mountOptions.host}` ];
|
||||
const sshArgs = sshOptions.concat([ 'cp', cpOptions, path.join(config.prefix, fromPath), path.join(config.prefix, toPath) ]);
|
||||
const sshArgs = sshOptions.concat([ 'cp', cpOptions, path.join(config.prefix ?? '', fromPath), path.join(config.prefix ?? '', toPath) ]);
|
||||
const [remoteCopyError] = await safe(shell.spawn('ssh', sshArgs, { shell: true }));
|
||||
safe.fs.unlinkSync(identityFilePath);
|
||||
if (!remoteCopyError) return;
|
||||
@@ -273,7 +273,7 @@ async function removeDir(config, limits, remotePathPrefix, progressCallback) {
|
||||
const identityFilePath = path.join(paths.SSHFS_KEYS_DIR, `identity_file_${path.basename(config._managedMountPath)}`);
|
||||
|
||||
const sshOptions = [ '-o', '"StrictHostKeyChecking no"', '-i', identityFilePath, '-p', config.mountOptions.port, `${config.mountOptions.user}@${config.mountOptions.host}` ];
|
||||
const sshArgs = sshOptions.concat([ 'rm', '-rf', path.join(config.prefix, remotePathPrefix) ]);
|
||||
const sshArgs = sshOptions.concat([ 'rm', '-rf', path.join(config.prefix ?? '', remotePathPrefix) ]);
|
||||
const [remoteRmError] = await safe(shell.spawn('ssh', sshArgs, { shell: true }));
|
||||
if (!remoteRmError) return;
|
||||
if (remoteRmError.code === 255) throw new BoxError(BoxError.EXTERNAL_ERROR, `SSH connection error: ${remoteRmError.message}`); // do not attempt fallback copy for ssh errors
|
||||
@@ -352,6 +352,8 @@ async function verifyConfig({ id, provider, config }) {
|
||||
if (path.isAbsolute(config.prefix)) throw new BoxError(BoxError.BAD_FIELD, 'prefix must be a relative path');
|
||||
if (path.normalize(config.prefix) !== config.prefix) throw new BoxError(BoxError.BAD_FIELD, 'prefix must contain a normalized relative path');
|
||||
}
|
||||
} else {
|
||||
config.prefix = '';
|
||||
}
|
||||
|
||||
if (provider === mounts.MOUNT_TYPE_FILESYSTEM) {
|
||||
|
||||
+5
-1
@@ -631,7 +631,11 @@ async function verifyConfig({ id, provider, config }) {
|
||||
|
||||
if (typeof config.prefix !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'prefix must be a string');
|
||||
if ('signatureVersion' in config && typeof config.signatureVersion !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'signatureVersion must be a string');
|
||||
if ('endpoint' in config && typeof config.endpoint !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'endpoint must be a string');
|
||||
if ('endpoint' in config) {
|
||||
if (typeof config.endpoint !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'endpoint must be a string');
|
||||
if (!config.endpoint.startsWith('http://') && !config.endpoint.startsWith('https://')) throw new BoxError(BoxError.BAD_FIELD, 'endpoint must start with http:// or https://');
|
||||
}
|
||||
|
||||
if ('region' in config && typeof config.region !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'region must be a string');
|
||||
|
||||
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean');
|
||||
|
||||
Reference in New Issue
Block a user