Compare commits

...

78 Commits

Author SHA1 Message Date
Johannes Zellner
a4ea80cf5e Use the full backup paths for sshfs remote copy
Fixes #889
2026-04-14 13:19:45 +02:00
Johannes Zellner
feacb58cd1 Only print readable shell.spawn() error details 2026-04-14 12:43:15 +02:00
Girish Ramakrishnan
1de30c0c38 reverseproxy: X-Content-Type-Options is worth keeping
looks like this has no modern replacement
2026-04-10 16:34:53 +02:00
Girish Ramakrishnan
4c30054a2d nginx: remove the various X- headers
these are all deprecated https://datatracker.ietf.org/doc/html/rfc6648
2026-04-10 16:08:20 +02:00
Girish Ramakrishnan
0b9e06c28d remove obsolete X-XSS-Protection
https://http.dev/x-xss-protection
2026-04-10 16:06:10 +02:00
Johannes Zellner
37e4a99ba6 Update dependencies 2026-04-09 16:37:53 +02:00
Girish Ramakrishnan
7078eb7482 use constants.DOCKER_IPv4_GATEWAY 2026-04-09 15:29:48 +02:00
Girish Ramakrishnan
c2ec97d641 mail: listen on the bridge IP
when requiresValidCertificate is set, we ended up injecting mutliple
IP addresses for my.domain.com - 172.18.0.1 (bridge) and the mail container IP.

Since the mail server is not running on the bridge, email may or may not be
sent depending on which IP is picked up by the app.

The solution is to make the mail container listen on the bridge as well.

The other solution might have been to introduce a new subdomain for mail container
and ensuring it is different from the dashboard subdomain. That way we can route
the requests to different IPs.
2026-04-09 15:25:19 +02:00
Girish Ramakrishnan
2a2a5ffb66 filesystem: remove shell usage
recent version of node throws this error:

(node:210013) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
2026-04-08 17:29:56 +02:00
Girish Ramakrishnan
b84ef57d58 appstore: language counts 2026-04-08 15:00:56 +02:00
Girish Ramakrishnan
14b066d3cd rename mountpoint to 'User-managed Mount Point'
this makes it clear that the user has to manage this
2026-04-08 13:30:52 +02:00
Johannes Zellner
2b5e167b07 Only update pankow 2026-04-07 18:01:15 +02:00
Johannes Zellner
c9547cbdb8 Improve app configure resource form states 2026-04-07 15:26:58 +02:00
Johannes Zellner
89a76148b4 Fix vue type casting warning 2026-04-07 14:53:44 +02:00
Girish Ramakrishnan
81fd472bb3 Fix typo crash 2026-04-07 13:21:48 +02:00
Girish Ramakrishnan
4ba9c63eb4 docker: attempt container start a few times
Docker Error: (HTTP code 500) server error - failed to set up container networking: driver failed programming external connectivity on endpoint a877975d-38be-4088-bc92-e0d7a486a818 (2e5adaa635a95bd65ca0f290712065d444528e3420c49f2f88323b40c62caaa5): failed to bind host port for 0.0.0.0:40014:172.18.16.130:40014/tcp: address already in use

This happens during app updates. Can only be two reasons:

- some race in docker not freeing up ports (unlikely)
- ephemeral port got reallocated between destroy and create as part of app update

A future commit will reserve net.ipv4.ip_local_reserved_ports as well

Similar fix as b08e3a5128
2026-04-07 13:04:56 +02:00
Girish Ramakrishnan
9e20c5a3e3 logs: escape and unescape new lines 2026-04-07 12:54:51 +02:00
Girish Ramakrishnan
20e0774df2 impersonate: just generate a random password
this way we don't let user set some insecure one. and this two step
passowrd generate is quite confusing (generate button becomes copy)
2026-04-07 12:18:16 +02:00
Girish Ramakrishnan
603244aa6a removed double progressbars 2026-04-07 11:53:54 +02:00
Girish Ramakrishnan
1cc30934c7 apppasswords: add loading state 2026-04-07 11:50:08 +02:00
Girish Ramakrishnan
053f26cd02 apppasswords: list oidc apps in the ui 2026-04-07 11:41:23 +02:00
Girish Ramakrishnan
cc82a088a9 apppassword: 16 lowercase letters in groups of 4, to make it easier to type 2026-04-07 11:01:43 +02:00
Girish Ramakrishnan
e30e384cec services: stop turn if unused by apps 2026-04-05 11:49:18 +02:00
Girish Ramakrishnan
33691a6507 schema: add missing fields 2026-04-05 11:12:06 +02:00
Girish Ramakrishnan
83917f98f5 backup sites: disable del in demo mode 2026-04-04 11:01:52 +02:00
Johannes Zellner
1fe5a61e52 Manually update tldjs rules when we create a release tarball 2026-04-03 15:24:33 +02:00
Johannes Zellner
dab9bcb9db Add local authserver to provide /verify-credentials route
This is used for apps which are using OpenID to login but still need to
be able to verify the users password or app password
2026-04-02 22:02:45 +02:00
Johannes Zellner
b2ca6206cc Fix dashboard lock file to work with node 24.13.0 2026-04-02 20:09:29 +02:00
Johannes Zellner
918c2f8587 Move to @cloudron/safetydance 2026-04-01 09:49:34 +02:00
Girish Ramakrishnan
8f851164d6 reboot: fix dashboard link 2026-04-01 09:25:10 +02:00
Johannes Zellner
d215d1998f Update docs link for tls provider 2026-03-31 13:51:08 +02:00
Girish Ramakrishnan
75e3256497 mail: update haraka to 3.1.4 2026-03-31 12:22:37 +02:00
Girish Ramakrishnan
58f5a17a83 mail: remove queue proxy
this has never worked well
2026-03-31 11:36:16 +02:00
Girish Ramakrishnan
e7c3d797be rsync: reupload files with corrupt integrity
we found sha256: null as the integrity in some of the cache files.
not sure how this happenned. for now, we just mark files with invalid
or missing sha256 for re-upload.
2026-03-31 11:31:17 +02:00
Girish Ramakrishnan
34abd5b8f5 9.1.6 changes 2026-03-30 14:40:26 +02:00
Girish Ramakrishnan
8b138d14bb backup site: remove the local disk provider
we already have ext4, xfs, mountpoint and filesystem to cover all cases

fixes #879
2026-03-30 14:37:48 +02:00
Johannes Zellner
e23abd69b5 Update frontend dependencies 2026-03-30 13:54:26 +02:00
Girish Ramakrishnan
9c16ad456d backups: set focus in the edit dialog 2026-03-30 13:52:54 +02:00
Girish Ramakrishnan
4b851afc6a location: show what DNS is being overwritten in location UI
fixes #858
2026-03-30 13:43:07 +02:00
Girish Ramakrishnan
f333148afa Update translations 2026-03-30 13:07:56 +02:00
Girish Ramakrishnan
8d0160a3e7 app configure: refresh app when a task is started 2026-03-30 10:25:26 +02:00
Girish Ramakrishnan
4a02e988c1 location: fix duplication of port bindings on submit 2026-03-30 09:47:05 +02:00
Girish Ramakrishnan
134472cd4b cloudron-support: services could be lazy-stopped 2026-03-28 14:46:00 +01:00
Girish Ramakrishnan
b40a10da7b restore: prune portBindings whose tcpPorts/udpPorts no longer exist
fixes #871
2026-03-27 18:47:52 +01:00
Girish Ramakrishnan
25f5b33d17 Remove unused secondaryDomains in update and restore code paths
fixes #814
2026-03-27 17:46:28 +01:00
Girish Ramakrishnan
f57c39bba2 repair: rebuild image 2026-03-27 16:17:41 +01:00
Girish Ramakrishnan
99b234eca8 source install: persist buildConfig so restore, import, clone work correctly 2026-03-27 16:10:43 +01:00
Girish Ramakrishnan
9c3c8cc9d1 rename promise-retry to retry 2026-03-27 11:39:38 +01:00
Girish Ramakrishnan
b08e3a5128 docker: attempt container recreate a few times
Docker Error: (HTTP code 500) server error - failed to set up container networking: driver failed programming external connectivity on endpoint a877975d-38be-4088-bc92-e0d7a486a818 (2e5adaa635a95bd65ca0f290712065d444528e3420c49f2f88323b40c62caaa5): failed to bind host port for 0.0.0.0:40014:172.18.16.130:40014/tcp: address already in use

This happens during app updates. Can only be two reasons:

- some race in docker not freeing up ports (unlikely)
- ephemeral port got reallocated between destroy and create as part of app update

A future commit will reserve net.ipv4.ip_local_reserved_ports as well
2026-03-27 10:29:26 +01:00
Girish Ramakrishnan
e48cdc85f7 notifications: subscribe owner and users to all by default 2026-03-27 09:14:18 +01:00
Johannes Zellner
a5da68a7f9 Fix overflow issue in eventlog 2026-03-26 16:15:47 +01:00
Johannes Zellner
7d594ab0d3 Also search for matches in app links labels for apps view filter 2026-03-25 22:58:55 +01:00
Johannes Zellner
9ed3d668ee Add .cursor to gitignore 2026-03-23 17:02:31 +01:00
Johannes Zellner
0da0a5e027 Show badges in header bar for expired or cancelled subs 2026-03-23 15:29:33 +01:00
Johannes Zellner
28eb0b65f4 Update dashboard dependencies 2026-03-23 11:44:12 +01:00
Johannes Zellner
1d29572ecd Wait for rest calls on app uninstall and archive 2026-03-23 11:43:25 +01:00
Johannes Zellner
07e8d242d1 fix vue warning, reactive() variables should not be const 2026-03-23 11:13:51 +01:00
Johannes Zellner
1586a286d8 Fix notifications scrolling 2026-03-23 10:42:30 +01:00
Girish Ramakrishnan
4859059eba source install: support dockerfileName and build options 2026-03-21 17:29:47 +01:00
Girish Ramakrishnan
f2949c1836 notifications: send email when manual app update is required 2026-03-21 15:59:41 +01:00
Girish Ramakrishnan
cd6acfb91d notifications: send email when manual platform update is required 2026-03-21 15:38:12 +01:00
Johannes Zellner
2d5dc9a6aa Fix wrong disabled state for devices config of apps 2026-03-21 08:54:42 +01:00
Girish Ramakrishnan
87e7da2aff community: auto focus to text input 2026-03-20 17:37:53 +01:00
Johannes Zellner
461eb38d88 Add comment why unused import exists 2026-03-18 14:49:18 +01:00
Johannes Zellner
ba0bb62fa3 hardcode CLI name for cid-cli in device auth flow 2026-03-18 14:37:15 +01:00
Johannes Zellner
1ca62dd38e Restyle oidc device login views 2026-03-18 14:28:28 +01:00
Johannes Zellner
1b1328c601 Fix more ejs usage in oidc device login views 2026-03-18 11:13:21 +01:00
Johannes Zellner
9633036887 vueify OIDC device views 2026-03-18 10:58:12 +01:00
Girish Ramakrishnan
e3d76ea9f4 uninstall: must continue to teardown other addons 2026-03-18 15:26:06 +05:30
Girish Ramakrishnan
d7212e69b5 unprovision: clear the default backup site 2026-03-18 15:14:11 +05:30
Girish Ramakrishnan
ead58bd6f6 test: use profile to check for passkey 2026-03-18 15:00:45 +05:30
Girish Ramakrishnan
fbe13b75df passkey: fix tests 2026-03-18 14:53:00 +05:30
Girish Ramakrishnan
6085a8231f uninstall: ignore services error as services may never have started 2026-03-18 14:38:47 +05:30
Johannes Zellner
e15cd190b3 Prevent user setup form if passwords dont match 2026-03-18 09:57:56 +01:00
Girish Ramakrishnan
3d55423deb Fix usage of safe() 2026-03-18 14:26:42 +05:30
Girish Ramakrishnan
f62df52c1d passkey: disallow in demo mode 2026-03-18 12:28:57 +05:30
Girish Ramakrishnan
7829f94ac4 update changelog 2026-03-18 11:16:15 +05:30
Girish Ramakrishnan
e9d42b9cdd migration: if no autoupdate setting, use defaults 2026-03-18 11:15:24 +05:30
283 changed files with 3035 additions and 1417 deletions

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ installer/src/certs/server.key
# vim swap files
*.swp
.cursor

38
CHANGES
View File

@@ -3186,3 +3186,41 @@
* passkey: fix issue where passkeys were lost on restart
* passkey: implement passwordless login
* oidcserver: fix jwks_rsaonly response
[9.1.5]
* services: lazy start services / on demand services
* restore: fix restore of trusted ips and blocklist
* dashboard: wait for dashboard reload when version has changed
* graphite: fix aggregation of block/network read/write
* Workaround chrome quirks on file drop handling
* notifications: add empty text, progress bar and inifinite scroll
* rsync: throttle log messages during download
* backup logs: make them much terse and concise
* oidc: implement Device Authorization Grant
* operator: fix viewing of backup progress and logs
* notification: automatic app update failure notification
* backup sites: identify conflicting site locations
* update: add policy to update apps and platform separately
* passkey: fix issue where passkeys were lost on restart
* passkey: implement passwordless login
* oidcserver: fix jwks_rsaonly response
[9.1.6]
* apps: fix wrong disabled state for devices config
* notifications: send email when manual platform and app update required
* source install: support dockerfileName and build options
* source install: persist buildConfig so restore, import, clone work correctly
* search for matches in app links labels for apps view filter
* restore: prune portBindings whose tcpPorts/udpPorts no longer exist
* location: fix duplication of port bindings on submit
* Update translations
* location: show what DNS is being overwritten in location UI
* backup site: remove the local disk provider
* mail: update haraka to 3.1.4, tika to 3.3.0
* solr: dynamically allocate java heap based on container mem
[9.2.0]
* apppasswords: generate easier to type passwords
* logs: escape and unescape new lines
* backups/volumes: rename 'mountpoint' to 'User-managed Mount Point'
* mail: listen on the bridge IP

11
box.js
View File

@@ -4,10 +4,11 @@ import constants from './src/constants.js';
import fs from 'node:fs';
import ldapServer from './src/ldapserver.js';
import net from 'node:net';
import authServer from './src/authserver.js';
import oidcServer from './src/oidcserver.js';
import paths from './src/paths.js';
import proxyAuth from './src/proxyauth.js';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import server from './src/server.js';
import directoryServer from './src/directoryserver.js';
import logger from './src/logger.js';
@@ -37,8 +38,10 @@ async function setupNetworking() {
function exitSync(status) {
const ts = new Date().toISOString();
if (status.message) fs.write(logFd, `${ts} ${status.message}\n`, function () {});
const msg = status.error.stack.replace(/\n/g, `\n${ts} `); // prefix each line with ts
if (status.error) fs.write(logFd, `${ts} ${msg}\n`, function () {});
if (status.error) {
const escapedStack = status.error.stack.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
fs.write(logFd, `${ts} ${escapedStack}\n`, function () {});
}
fs.fsyncSync(logFd);
fs.closeSync(logFd);
process.exit(status.code);
@@ -72,6 +75,7 @@ process.on('SIGINT', async function () {
await directoryServer.stop();
await ldapServer.stop();
await oidcServer.stop();
await authServer.stop();
setTimeout(() => {
log('Shutdown complete');
@@ -87,6 +91,7 @@ process.on('SIGTERM', async function () {
await directoryServer.stop();
await ldapServer.stop();
await oidcServer.stop();
await authServer.stop();
setTimeout(() => {
log('Shutdown complete');

View File

@@ -1,37 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Confirm Device</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
body { font-family: system-ui, sans-serif; margin-top: 25px; margin-bottom: 25px; }
h1, h1+p { font-weight: 100; text-align: center; }
.container { padding: 0 40px 10px; width: 274px; background-color: #f7f7f7; margin: 0 auto 10px; border-radius: 2px; box-shadow: 0 2px 2px rgba(0,0,0,.3); overflow: hidden; }
h1 { font-size: 2.3em; }
code { font-size: 2em; }
button[autofocus] { width: 100%; display: block; margin-bottom: 10px; font-size: 14px; font-weight: 700; height: 36px; padding: 0 8px; border: 0; color: #fff; background-color: #4d90fe; cursor: pointer; }
button[autofocus]:hover { background-color: #357ae8; }
button[name=abort] { background: none; border: none; padding: 0; font: inherit; cursor: pointer; color: #666; opacity: .6; }
.help { width: 100%; font-size: 12px; text-align: center; }
</style>
</head>
<body>
<div class="container">
<h1>Confirm Device</h1>
<p>
<strong><%= clientName %></strong>
<br/><br/>
The following code should be displayed on your device<br/><br/>
<code><%= userCode %></code>
<br/><br/>
<small>If you did not initiate this action or the code does not match, please close this window or click abort.</small>
</p>
<%- form %>
<button autofocus type="submit" form="op.deviceConfirmForm">Continue</button>
<div class="help">
<button type="submit" form="op.deviceConfirmForm" value="yes" name="abort">[ Abort ]</button>
</div>
</div>
</body>
<head>
<title>OpenID Confirm</title>
<script>
window.cloudron = <%- JSON.stringify({ name, clientName, userCode, form }) %>;
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/oidcdeviceconfirm.js"></script>
</body>
</html>

View File

@@ -1,27 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sign-in</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
body { font-family: system-ui, sans-serif; margin-top: 25px; margin-bottom: 25px; }
h1, h1+p { font-weight: 100; text-align: center; }
.container { padding: 0 40px 10px; width: 274px; background-color: #f7f7f7; margin: 0 auto 10px; border-radius: 2px; box-shadow: 0 2px 2px rgba(0,0,0,.3); overflow: hidden; }
h1 { font-size: 2.3em; }
p.red { color: #d50000; }
input[type=text] { height: 44px; font-size: 16px; width: 100%; margin-bottom: 10px; background: #fff; border: 1px solid #d9d9d9; border-top: 1px solid silver; padding: 0 8px; box-sizing: border-box; text-transform: uppercase; text-align: center; }
input[type=text]::placeholder { text-transform: none; }
[type=submit] { width: 100%; display: block; margin-bottom: 10px; text-align: center; font-size: 14px; font-weight: 700; height: 36px; padding: 0 8px; border: 0; color: #fff; background-color: #4d90fe; cursor: pointer; }
[type=submit]:hover { background-color: #357ae8; }
</style>
</head>
<body>
<div class="container">
<h1>Sign-in</h1>
<%- message %>
<%- form %>
<button type="submit" form="op.deviceInputForm">Continue</button>
</div>
</body>
<head>
<title>OpenID Device Sign-in</title>
<script>
window.cloudron = <%- JSON.stringify({ name, message, form }) %>;
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/oidcdeviceinput.js"></script>
</body>
</html>

View File

@@ -1,20 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
body { font-family: system-ui, sans-serif; margin-top: 25px; margin-bottom: 25px; }
h1, h1+p { font-weight: 100; text-align: center; }
.container { padding: 0 40px 10px; width: 274px; background-color: #f7f7f7; margin: 0 auto 10px; border-radius: 2px; box-shadow: 0 2px 2px rgba(0,0,0,.3); overflow: hidden; }
h1 { font-size: 2.3em; }
</style>
</head>
<body>
<div class="container">
<h1>Success</h1>
<p>Your device has been authorized. You can close this window.</p>
</div>
</body>
<head>
<title>OpenID Device Success</title>
<script>
window.cloudron = <%- JSON.stringify({ name }) %>;
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/oidcdevicesuccess.js"></script>
</body>
</html>

View File

@@ -4,13 +4,7 @@
<title><%= name %> OpenID Error</title>
<script>
window.cloudron = <%- JSON.stringify({
iconUrl: iconUrl,
name: name,
errorMessage: errorMessage,
footer: footer,
language: language
}) %>;
window.cloudron = <%- JSON.stringify({ iconUrl, name, errorMessage, footer, language }) %>;
</script>
</head>

View File

@@ -5,7 +5,7 @@
"packages": {
"": {
"dependencies": {
"@cloudron/pankow": "^4.1.5",
"@cloudron/pankow": "^4.1.10",
"@fontsource/inter": "^5.2.8",
"@fortawesome/fontawesome-free": "^7.2.0",
"@simplewebauthn/browser": "^13.3.0",
@@ -17,16 +17,16 @@
"async": "^3.2.6",
"chart.js": "^4.5.1",
"chartjs-plugin-annotation": "^3.1.0",
"eslint": "^10.0.3",
"eslint": "^10.2.0",
"eslint-plugin-vue": "^10.8.0",
"marked": "^17.0.4",
"marked": "^18.0.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"vite": "^8.0.0",
"moment-timezone": "^0.6.1",
"vite": "^8.0.8",
"vite-plugin-singlefile": "^2.3.2",
"vue": "^3.5.30",
"vue-i18n": "^11.3.0",
"vue-router": "^5.0.3"
"vue": "^3.5.32",
"vue-i18n": "^11.3.2",
"vue-router": "^5.0.4"
}
},
"node_modules/@babel/generator": {
@@ -92,43 +92,22 @@
}
},
"node_modules/@cloudron/pankow": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-4.1.5.tgz",
"integrity": "sha512-VPnRXQ6m9jSV07e8Hwg/t+If/POYlzym+fuSd9L/13uP+uDovUzVqvXWmPPjYeyNkRvXQAxJv6IMXZ8Q74wFxw==",
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-4.1.10.tgz",
"integrity": "sha512-MQ2320U/zZdQtidDgjkBvZxXrm/hJtdjHxt1Ww1hgPTOS6UVxpcUTpXuM/68d9xJTZQC4lK+EeFW4A8Vu+K/sg==",
"license": "ISC",
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@fortawesome/fontawesome-free": "^7.2.0",
"filesize": "^11.0.13",
"filesize": "^11.0.15",
"monaco-editor": "^0.55.1",
"online-3d-viewer": "^0.18.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -175,12 +154,12 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.23.3",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
"integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
"version": "0.23.5",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^3.0.3",
"@eslint/object-schema": "^3.0.5",
"debug": "^4.3.1",
"minimatch": "^10.2.4"
},
@@ -189,21 +168,21 @@
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
"integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
"integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==",
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.1.1"
"@eslint/core": "^1.2.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
"integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
@@ -213,21 +192,21 @@
}
},
"node_modules/@eslint/object-schema": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
"integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
"integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
"integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
"integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.1.1",
"@eslint/core": "^1.2.1",
"levn": "^0.4.1"
},
"engines": {
@@ -301,14 +280,14 @@
}
},
"node_modules/@intlify/core-base": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
"integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.2.tgz",
"integrity": "sha512-cgsUaV/dyD6aS49UPgerIblrWeXAZHNaDWqm4LujOGC7IafSyhghGXEiSVvuDYaDPiQTP+tSFSTM1HIu7Yp1nA==",
"license": "MIT",
"dependencies": {
"@intlify/devtools-types": "11.3.0",
"@intlify/message-compiler": "11.3.0",
"@intlify/shared": "11.3.0"
"@intlify/devtools-types": "11.3.2",
"@intlify/message-compiler": "11.3.2",
"@intlify/shared": "11.3.2"
},
"engines": {
"node": ">= 16"
@@ -318,13 +297,13 @@
}
},
"node_modules/@intlify/devtools-types": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz",
"integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==",
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.2.tgz",
"integrity": "sha512-q96G2ZZw0FNoXzejbjIf9dbfgz1xyYBZu6ZT4b5TE/55j8d1O9X5jv0k+U+L3fVe7uebPcqRQFD0ffm30i5mJA==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.3.0",
"@intlify/shared": "11.3.0"
"@intlify/core-base": "11.3.2",
"@intlify/shared": "11.3.2"
},
"engines": {
"node": ">= 16"
@@ -334,12 +313,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
"integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.2.tgz",
"integrity": "sha512-d/awyHUkNSaGPxBxT/qlUpfRizxHX9dt55CnW03xx5p1KmMyfYHKupCnvzINX+Na8JR8LAR7y32lPKjoeQGmzA==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.3.0",
"@intlify/shared": "11.3.2",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -350,9 +329,9 @@
}
},
"node_modules/@intlify/shared": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
"integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.2.tgz",
"integrity": "sha512-x66fjdH6i+lNYPae5URSQGTjBL68Av6hi09jvC5Ci96iTkwfqrPhCj46aylQZmgMaG89rOZCIKqS7ApC8ZDVjg==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -413,43 +392,36 @@
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@oxc-project/runtime": {
"version": "0.115.0",
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@oxc-project/types": {
"version": "0.115.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
"integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
"cpu": [
"arm64"
],
@@ -463,9 +435,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
"integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
"cpu": [
"arm64"
],
@@ -479,9 +451,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
"integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
"cpu": [
"x64"
],
@@ -495,9 +467,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
"integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
"cpu": [
"x64"
],
@@ -511,9 +483,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
"integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
"cpu": [
"arm"
],
@@ -527,9 +499,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
"cpu": [
"arm64"
],
@@ -543,9 +515,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
"integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
"cpu": [
"arm64"
],
@@ -559,9 +531,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
"cpu": [
"ppc64"
],
@@ -575,9 +547,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
"cpu": [
"s390x"
],
@@ -591,9 +563,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
"cpu": [
"x64"
],
@@ -607,9 +579,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
"integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
"cpu": [
"x64"
],
@@ -623,9 +595,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
"integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
"cpu": [
"arm64"
],
@@ -639,25 +611,27 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
"integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.1"
"@emnapi/core": "1.9.2",
"@emnapi/runtime": "1.9.2",
"@napi-rs/wasm-runtime": "^1.1.3"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
"integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
"cpu": [
"arm64"
],
@@ -671,9 +645,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
"integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"cpu": [
"x64"
],
@@ -1112,39 +1086,39 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
"integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
"integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@vue/shared": "3.5.30",
"@babel/parser": "^7.29.2",
"@vue/shared": "3.5.32",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
"integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
"integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.30",
"@vue/shared": "3.5.30"
"@vue/compiler-core": "3.5.32",
"@vue/shared": "3.5.32"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
"integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
"integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@vue/compiler-core": "3.5.30",
"@vue/compiler-dom": "3.5.30",
"@vue/compiler-ssr": "3.5.30",
"@vue/shared": "3.5.30",
"@babel/parser": "^7.29.2",
"@vue/compiler-core": "3.5.32",
"@vue/compiler-dom": "3.5.32",
"@vue/compiler-ssr": "3.5.32",
"@vue/shared": "3.5.32",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.8",
@@ -1152,13 +1126,13 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
"integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
"integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.30",
"@vue/shared": "3.5.30"
"@vue/compiler-dom": "3.5.32",
"@vue/shared": "3.5.32"
}
},
"node_modules/@vue/devtools-api": {
@@ -1186,53 +1160,53 @@
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
"integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
"integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.30"
"@vue/shared": "3.5.32"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
"integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
"integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.30",
"@vue/shared": "3.5.30"
"@vue/reactivity": "3.5.32",
"@vue/shared": "3.5.32"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
"integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
"integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.30",
"@vue/runtime-core": "3.5.30",
"@vue/shared": "3.5.30",
"@vue/reactivity": "3.5.32",
"@vue/runtime-core": "3.5.32",
"@vue/shared": "3.5.32",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
"integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
"integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.30",
"@vue/shared": "3.5.30"
"@vue/compiler-ssr": "3.5.32",
"@vue/shared": "3.5.32"
},
"peerDependencies": {
"vue": "3.5.30"
"vue": "3.5.32"
}
},
"node_modules/@vue/shared": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
"integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
"license": "MIT"
},
"node_modules/@xterm/addon-attach": {
@@ -1363,9 +1337,9 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -1538,18 +1512,18 @@
}
},
"node_modules/eslint": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz",
"integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz",
"integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.3",
"@eslint/config-helpers": "^0.5.2",
"@eslint/core": "^1.1.1",
"@eslint/plugin-kit": "^0.6.1",
"@eslint/config-array": "^0.23.4",
"@eslint/config-helpers": "^0.5.4",
"@eslint/core": "^1.2.0",
"@eslint/plugin-kit": "^0.7.0",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@@ -1560,7 +1534,7 @@
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^9.1.2",
"eslint-visitor-keys": "^5.0.1",
"espree": "^11.1.1",
"espree": "^11.2.0",
"esquery": "^1.7.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -1779,9 +1753,9 @@
}
},
"node_modules/filesize": {
"version": "11.0.13",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.13.tgz",
"integrity": "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw==",
"version": "11.0.15",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.15.tgz",
"integrity": "sha512-30TpbYxQxCpi4XdVjkwXYQ37CzZltV38+P7MYroQ+4NK/Dmx9mxixFNrolzcmEIBsjT/uowC9T7kiy2+C12r1A==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 10.8.0"
@@ -2290,9 +2264,9 @@
}
},
"node_modules/marked": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
"integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.0.tgz",
"integrity": "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@@ -2327,12 +2301,12 @@
}
},
"node_modules/minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -2380,9 +2354,9 @@
}
},
"node_modules/moment-timezone": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.0.tgz",
"integrity": "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.1.tgz",
"integrity": "sha512-1B9lmAhB9D9/sHaPC1N7wLFEVUoFldxOpOO96lOD1PvJ43vCd0ozDPbu0FEL3++VvawOlDkq8YD373tJmP5JHw==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
@@ -2562,9 +2536,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -2673,13 +2647,13 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.115.0",
"@rolldown/pluginutils": "1.0.0-rc.9"
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -2688,27 +2662,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.9",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
"@rolldown/binding-darwin-x64": "1.0.0-rc.9",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"license": "MIT"
},
"node_modules/rollup": {
@@ -2909,17 +2883,16 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/runtime": "0.115.0",
"lightningcss": "^1.32.0",
"picomatch": "^4.0.3",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.9",
"rolldown": "1.0.0-rc.15",
"tinyglobby": "^0.2.15"
},
"bin": {
@@ -2936,8 +2909,8 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.0.0-alpha.31",
"esbuild": "^0.27.0",
"@vitejs/devtools": "^0.1.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
@@ -3004,17 +2977,17 @@
}
},
"node_modules/vue": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.30",
"@vue/compiler-sfc": "3.5.30",
"@vue/runtime-dom": "3.5.30",
"@vue/server-renderer": "3.5.30",
"@vue/shared": "3.5.30"
"@vue/compiler-dom": "3.5.32",
"@vue/compiler-sfc": "3.5.32",
"@vue/runtime-dom": "3.5.32",
"@vue/server-renderer": "3.5.32",
"@vue/shared": "3.5.32"
},
"peerDependencies": {
"typescript": "*"
@@ -3050,14 +3023,14 @@
}
},
"node_modules/vue-i18n": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.2.tgz",
"integrity": "sha512-gmFrvM+iuf2AH4ygligw/pC7PRJ63AdRNE68E0GPlQ83Mzfyck6g6cRQC3KzkYXr+ZidR91wq+5YBmAMpkgE1A==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.3.0",
"@intlify/devtools-types": "11.3.0",
"@intlify/shared": "11.3.0",
"@intlify/core-base": "11.3.2",
"@intlify/devtools-types": "11.3.2",
"@intlify/shared": "11.3.2",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
@@ -3071,9 +3044,9 @@
}
},
"node_modules/vue-router": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz",
"integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz",
"integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.28.6",

View File

@@ -8,7 +8,7 @@
"type": "module",
"dependencies": {
"@simplewebauthn/browser": "^13.3.0",
"@cloudron/pankow": "^4.1.5",
"@cloudron/pankow": "^4.1.10",
"@fontsource/inter": "^5.2.8",
"@fortawesome/fontawesome-free": "^7.2.0",
"@vitejs/plugin-vue": "^6.0.5",
@@ -19,15 +19,15 @@
"async": "^3.2.6",
"chart.js": "^4.5.1",
"chartjs-plugin-annotation": "^3.1.0",
"eslint": "^10.0.3",
"eslint": "^10.2.0",
"eslint-plugin-vue": "^10.8.0",
"marked": "^17.0.4",
"marked": "^18.0.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"vite": "^8.0.0",
"moment-timezone": "^0.6.1",
"vite": "^8.0.8",
"vite-plugin-singlefile": "^2.3.2",
"vue": "^3.5.30",
"vue-i18n": "^11.3.0",
"vue-router": "^5.0.3"
"vue": "^3.5.32",
"vue-i18n": "^11.3.2",
"vue-router": "^5.0.4"
}
}

View File

@@ -48,7 +48,9 @@
"configure": "Nakonfigurovat",
"restart": "Restartovat",
"reset": "Zresetovat",
"loadMore": "Načíst více"
"loadMore": "Načíst více",
"setup": "Nastavit",
"disable": "Zakázat"
},
"rebootDialog": {
"title": "Restart serveru",
@@ -361,8 +363,23 @@
},
"twoFactorAuth": {
"title": "Dvoufaktorová autentizace",
"totpEnabled": "Použít časově omezené jednorázové heslo (TOTP)",
"passkeyEnabled": "Použít passkey"
"totpEnabled": "Povoleno",
"passkeyEnabled": "Povoleno",
"totpTitle": "TOTP",
"passkeyTitle": "Passkey"
},
"notSet": "nenastaveno",
"enablePasskey": {
"title": "Povolit passkey"
},
"enableTotp": {
"title": "Povolit TOTP"
},
"disableTotp": {
"title": "Zakázat TOTP"
},
"disablePasskey": {
"title": "Zakázat passkey"
}
},
"backups": {
@@ -648,7 +665,10 @@
"stopUpdateAction": "Zastavit aktualizaci",
"disabled": "Zakázáno",
"onLatest": "poslední",
"description": "Aktualizace platformy a aplikací se aplikují v nastavený čas podle <a href=\"/#/system-settings\">časové zóny systému</a>."
"description": "Aktualizace se aplikují v nastavený čas podle <a href=\"/#/system-settings\">časové zóny systému</a>.",
"config": "Automatické aktualizace",
"platformAndApps": "Platforma a aplikace",
"appsOnly": "Pouze aplikace"
},
"updateScheduleDialog": {
"disableCheckbox": "Zakázat automatické aktualizace",
@@ -673,6 +693,14 @@
"registryConfig": {
"provider": "Poskytovatel docker registrů",
"providerOther": "Jiné"
},
"configureUpdates": {
"title": "Konfigurovat automatické aktualizace",
"policy": "Politika",
"policyDescription": "Vyberte, co se bude automaticky aktualizovat",
"days": "Dnů/y",
"hours": "Hodin/y",
"schedule": "Plány záloh"
}
},
"branding": {
@@ -884,7 +912,8 @@
"rebootRequired": "Vyžadován restart serveru",
"cloudronUpdateFailed": "Aktualizace Cloudronu selhala",
"diskSpace": "Nedostatek místa na disku",
"appAutoUpdateFailed": "Automatická aktualizace aplikace selhala"
"appAutoUpdateFailed": "Automatická aktualizace aplikace selhala",
"manualUpdateRequired": "Platforma nebo aplikace vyžaduje ruční aktualizaci"
},
"settingsDialog": {
"description": "Na váš primární e-mail bude odeslán e-mail souhrn těchto vybraných událostí."
@@ -941,10 +970,10 @@
"noRedirections": "Žádné přesměrované domény",
"addRedirectionAction": "Přidat přesměrování",
"saveAction": "Uložit",
"dnsoverwrite": "Některé DNS záznamy již existují. Potvrďte přepsání.",
"aliases": "Aliasy",
"addAliasAction": "Přidat alias",
"noAliases": "Žádné aliasy pro domény"
"noAliases": "Žádné aliasy pro domény",
"overwriteDns": "Přepsat existující DNS záznamy pro {domains}"
},
"accessControl": {
"userManagement": {
@@ -1594,7 +1623,9 @@
"errorIncorrect2FAToken": "Token 2FA je neplatný",
"errorInternal": "Interní chyba, zkuste akci opakovat později",
"loginAction": "Přihlásit se",
"usePasskeyAction": "Použít passkey"
"usePasskeyAction": "Použít passkey",
"passkeyAction": "Přihlásit se přes passkey",
"errorPasskeyFailed": "Přihlášení pomocí passkey selhalo"
},
"passwordReset": {
"title": "Reset hesla",

View File

@@ -1414,8 +1414,7 @@
"addRedirectionAction": "Eine Weiterleitung hinzufügen",
"noAliases": "Keine Aliasse",
"addAliasAction": "Alias hinzufügen",
"aliases": "Aliasse",
"dnsoverwrite": "Einige DNS-Einträge existieren bereits. Mit dem Überschreiben einverstanden."
"aliases": "Aliasse"
},
"updateDialog": {
"subscriptionExpired": "Das Cloudron-Abonnement ist abgelaufen. Bitte ein Abonnement einrichten, um die Anwendung zu aktualisieren.",

View File

@@ -912,7 +912,8 @@
"rebootRequired": "Server reboot required",
"cloudronUpdateFailed": "Cloudron update failed",
"diskSpace": "Low disk space",
"appAutoUpdateFailed": "App automatic update failed"
"appAutoUpdateFailed": "App automatic update failed",
"manualUpdateRequired": "Platform or app requires manual update"
},
"settingsDialog": {
"description": "An email will be sent for the selected events to your primary email."
@@ -1229,7 +1230,7 @@
"aliases": "Aliases",
"addAliasAction": "Add an alias",
"noAliases": "No alias domains",
"dnsoverwrite": "Some DNS records already exist. Agree to overwrite."
"overwriteDns": "Overwrite existing DNS records of {domains}"
},
"accessControl": {
"userManagement": {
@@ -1633,7 +1634,8 @@
"editVolumeDialog": {
"title": "Edit Volume"
},
"emptyPlaceholder": "No volumes"
"emptyPlaceholder": "No volumes",
"mountPointDescription": "The mount point has to be set up manually. See <a href=\"{{ docsLink }}\" target=\"_blank\">docs</a>."
},
"newLoginEmail": {
"subject": "[<%= cloudron %>] New login on your account",

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,8 @@
"email": "Se connecter avec une adresse email",
"sso": "Se connecter avec vos identifiants Cloudron",
"openid": "Se connecter avec Cloudron OpenID"
}
},
"noMatchesPlaceholder": "Aucune application correspondante"
},
"main": {
"offline": "Cloudron est hors ligne. Reconnexion…",
@@ -26,14 +27,26 @@
"save": "Sauvegarder",
"no": "Non",
"yes": "Oui",
"delete": "Supprimer"
"delete": "Supprimer",
"edit": "Editer",
"done": "Terminer"
},
"username": "Nom d'utilisateur",
"actions": "Actions",
"displayName": "Nom affiché",
"action": {
"logs": "Journaux",
"reboot": "Redémarrer"
"reboot": "Redémarrer",
"remove": "Supprimer",
"edit": "Editer",
"add": "Ajouter",
"next": "Suivant",
"configure": "Configurer",
"restart": "Redémarrer",
"reset": "Réinitialiser",
"loadMore": "Charger plus",
"setup": "Installer",
"disable": "Désactiver"
},
"rebootDialog": {
"rebootAction": "Redémarrer maintenant",
@@ -47,9 +60,20 @@
},
"statusEnabled": "Activé",
"navbar": {
"users": "Utilisateurs"
"users": "Utilisateurs",
"groups": "Groupes"
},
"loadingPlaceholder": "Chargement"
"loadingPlaceholder": "Chargement",
"table": {
"version": "Version",
"created": "Créé"
},
"sidebar": {
"collapseAction": "Réduire la barre latérale"
},
"platform": {
"startupFailed": "Échec du démarrage de la plateforme"
}
},
"users": {
"users": {
@@ -64,17 +88,22 @@
"externalLdapTooltip": "Depuis un annuaire LDAP externe",
"setGhostTooltip": "Emprunter l'identité",
"invitationTooltip": "Inviter",
"mailmanagerTooltip": "Cet utilisateur peut gérer les utilisateurs et les boîtes mail"
"mailmanagerTooltip": "Cet utilisateur peut gérer les utilisateurs et les boîtes mail",
"noMatchesPlaceholder": "Aucun utilisateur correspondant",
"emptyPlaceholder": "Aucun utilisateur"
},
"groups": {
"name": "Nom",
"users": "Utilisateurs",
"externalLdapTooltip": "Depuis un annuaire LDAP externe"
"externalLdapTooltip": "Depuis un annuaire LDAP externe",
"emptyPlaceholder": "Aucun groupe",
"noMatchesPlaceholder": "Aucun groupe correspondant"
},
"settings": {
"allowProfileEditCheckbox": "Autoriser les utilisateurs à modifier leur nom et leur adresse email",
"saveAction": "Enregistrer",
"require2FACheckbox": "Demander aux utilisateurs une authentification à deux facteurs (2FA)"
"require2FACheckbox": "Demander aux utilisateurs une authentification à deux facteurs (2FA)",
"title": "Paramètres"
},
"externalLdap": {
"configureAction": "Paramétrer",
@@ -128,7 +157,8 @@
"group": {
"users": "Utilisateurs",
"name": "Nom",
"addGroupAction": "Ajouter un groupe"
"addGroupAction": "Ajouter un groupe",
"allowedApps": "Applications autorisées"
},
"deleteGroupDialog": {
"title": "Supprimer le groupe {{ name }}",
@@ -191,7 +221,14 @@
"placeholder": "Adresse IP séparée par ligne ou sous-réseau",
"label": "Accès restreint"
},
"cloudflarePortWarning": "Le proxy Cloudflare doit être désactivé sur le domaine du tableau de bord pour accéder au service LDAP"
"cloudflarePortWarning": "Le proxy Cloudflare doit être désactivé sur le domaine du tableau de bord pour accéder au service LDAP",
"enable": "Activer le serveur LDAP",
"title": "Serveur LDAP",
"enabled": "Activer le serveur LDAP"
},
"title": "Utilisateurs",
"2FAResetDialog": {
"title": "Réinitialiser l'authentification à deux facteurs de l'utilisateur"
}
},
"profile": {
@@ -216,7 +253,8 @@
"name": "Nom",
"noPasswordsPlaceholder": "Aucun mot de passe d'application créé",
"title": "Mots de passe d'application",
"description": "Les mots de passe d'application sont une mesure de sécurité pour protéger votre compte utilisateur Cloudron. Si vous avez besoin d'accéder à une application Cloudron depuis une application mobile ou un client auquel vous ne faites pas confiance, vous pouvez vous connecter avec votre nom d'utilisateur et le mot de passe alternatif généré ici."
"description": "Les mots de passe d'application sont une mesure de sécurité pour protéger votre compte utilisateur Cloudron. Si vous avez besoin d'accéder à une application Cloudron depuis une application mobile ou un client auquel vous ne faites pas confiance, vous pouvez vous connecter avec votre nom d'utilisateur et le mot de passe alternatif généré ici.",
"expires": "Date d'expiration"
},
"changeEmail": {
"title": "Modifier l'adresse email principale",
@@ -228,7 +266,8 @@
"app": "Application",
"name": "Nom du mot de passe",
"title": "Créer un mot de passe d'application",
"description": "Utilisez le mot de passe suivant pour vous authentifier auprès de l'application :"
"description": "Utilisez le mot de passe suivant pour vous authentifier auprès de l'application :",
"expiresAt": "Date d'expiration"
},
"changeFallbackEmail": {
"title": "Modifier l'adresse email de récupération du mot de passe"
@@ -237,14 +276,20 @@
"token": "Jeton",
"title": "Activer l'authentification à deux facteurs (2FA)",
"enable": "Activer",
"authenticatorAppDescription": "Scannez le code avec Google Authenticator (<a href=\"{{ googleAuthenticatorPlayStoreLink }}\" target=\"_blank\">Android</a>, <a href=\"{{ googleAuthenticatorITunesLink }}\" target=\"_blank\">iOS</a>), FreeOTP (<a href=\"{{ freeOTPPlayStoreLink }}\" target=\"_blank\">Android</a>, <a href=\"{{ freeOTPITunesLink }}\" target=\"_blank\">iOS</a>) ou une application d'authentification similaire."
"authenticatorAppDescription": "Scannez le code avec Google Authenticator (<a href=\"{{ googleAuthenticatorPlayStoreLink }}\" target=\"_blank\">Android</a>, <a href=\"{{ googleAuthenticatorITunesLink }}\" target=\"_blank\">iOS</a>), FreeOTP (<a href=\"{{ freeOTPPlayStoreLink }}\" target=\"_blank\">Android</a>, <a href=\"{{ freeOTPITunesLink }}\" target=\"_blank\">iOS</a>) ou une application d'authentification similaire.",
"mandatorySetup": "L'authentification à deux facteurs (2FA) est requise pour accéder au tableau de bord. Veuillez terminer la configuration pour continuer.",
"passkeyOption": "Clé d'accès",
"totpOption": "TOTP",
"registerPasskey": "Installer une clé d'accès",
"passkeyDescription": "Le navigateur vous invitera à créer une clé d'accès à l'aide des données biométriques de votre appareil ou d'un gestionnaire de mots de passe."
},
"createApiToken": {
"name": "Nom du jeton API",
"description": "Nouveau jeton API :",
"title": "Créer un jeton API",
"copyNow": "Veillez à copier le jeton API maintenant. Il ne s'affichera plus pour des raisons de sécurité.",
"access": "Accès API"
"access": "Accès API",
"allowedIpRanges": "Plage(s) d'adresses IP autorisées"
},
"changePasswordAction": "Modifier le mot de passe",
"apiTokens": {
@@ -256,7 +301,9 @@
"lastUsed": "Dernière utilisation",
"scope": "Portée",
"readonly": "Lecture seule",
"readwrite": "Lecture et écriture"
"readwrite": "Lecture et écriture",
"allowedIpRangesPlaceholder": "Adresses IP ou sous-réseaux séparés par des virgules",
"allowedIpRanges": "Adresses IP autorisées"
},
"loginTokens": {
"logoutAll": "Déconnecter de tous",
@@ -265,6 +312,32 @@
},
"passwordResetNotification": {
"body": "Email envoyé à {{ email }}"
},
"removeApiToken": {
"title": "Supprimer le jeton API"
},
"removeAppPassword": {
"title": "Supprimer le mot de passe de l'application"
},
"twoFactorAuth": {
"title": "Authentification à deux facteurs",
"totpEnabled": "Activé",
"passkeyEnabled": "Activé",
"totpTitle": "TOTP",
"passkeyTitle": "Clé d'accès"
},
"notSet": "Non défini",
"enablePasskey": {
"title": "Activer la clé d'accès"
},
"enableTotp": {
"title": "Activer le TOTP"
},
"disableTotp": {
"title": "Désactiver le TOTP"
},
"disablePasskey": {
"title": "Désactiver la clé d'accès"
}
},
"backups": {
@@ -276,7 +349,9 @@
"days": "Jours",
"hours": "Heures",
"title": "Paramétrer la planification et la conservation des sauvegardes",
"retentionPolicy": "Politique de conservation"
"retentionPolicy": "Politique de conservation",
"disable": "Désactiver les sauvegardes automatiques",
"enable": "Activer les sauvegardes automatiques"
},
"schedule": {
"title": "Planification et conservation",
@@ -324,13 +399,36 @@
"port": "Port",
"cifsSealSupport": "Utilisez le cryptage du sceau. Nécessite au moins SMB v3",
"chown": "Le système de fichiers distant prend en charge chown",
"encryptFilenames": "Chiffré les nom de fichiers"
"encryptFilenames": "Chiffré les nom de fichiers",
"preserveAttributesLabel": "Conserver les attributs du fichier",
"name": "Nom",
"encryptionHint": "Indice pour le mot de passe de chiffrement",
"usesEncryption": "La sauvegarde est chiffrée",
"useForUpdates": "Enregistrer ici les sauvegardes des mises à jour automatiques",
"backupContents": {
"title": "Contenu de la sauvegarde",
"description": "Choisissez les éléments à sauvegarder sur ce site.",
"everything": "Tout",
"excludeSelected": "Exclure les éléments sélectionnés",
"includeOnlySelected": "N'inclure que les éléments sélectionnés"
},
"automaticUpdates": {
"title": "Sauvegardes des mises à jour automatiques"
},
"useEncryption": "Chiffrer les sauvegardes",
"regionHelperText": "Par défaut \"us-east-1\" si laissé vide",
"prefixHelperText": "Les sauvegardes sont stockées dans ce sous-dossier"
},
"backupDetails": {
"title": "Informations sur la sauvegarde",
"id": "ID",
"date": "Date",
"version": "Version"
"version": "Version",
"size": "Taille",
"duration": "Durée de la sauvegarde",
"lastIntegrityCheck": "Dernier contrôle d'intégrité",
"integrityNever": "Jamais",
"integrityInProgress": "En cours"
},
"listing": {
"title": "Liste",
@@ -352,12 +450,45 @@
"tooltip": "Cela préservera également le courrier et les sauvegardes d'application {{ appsLength }}."
},
"remotePath": "Chemin d'accès à distance"
}
},
"archives": {
"title": "Archive de l'application",
"info": "Information"
},
"deleteArchiveDialog": {
"title": "Supprimer l'archive"
},
"deleteArchive": {
"deleteAction": "Supprimer"
},
"restoreArchiveDialog": {
"title": "Restaurer à partir de l'archive",
"restoreAction": "Restaurer",
"restoreActionOverwrite": "Restaurer et écraser le DNS"
},
"sites": {
"title": "Sites"
},
"site": {
"addDialog": {
"title": "Ajouter un site de sauvegarde"
}
},
"configAction": "Configuration",
"contentAction": "Contenu",
"configureContent": {
"title": "Configurer le contenu de la sauvegarde"
},
"useFileAndFileNameEncryption": "Chiffrement des fichiers et des noms de fichiers utilisé",
"useFileEncryption": "Chiffrement des fichiers utilisé",
"checkIntegrity": "Vérifier l'intégrité",
"stopIntegrity": "Arrêter le contrôle d'intégrité"
},
"emails": {
"title": "Messagerie",
"changeDomainDialog": {
"description": "Cela déplacera le serveur IMAP et SMTP vers l'emplacement choisi."
"description": "Cela déplacera le serveur IMAP et SMTP vers l'emplacement choisi.",
"setAction": "Définir l'emplacement"
},
"eventlog": {
"details": "Détails",
@@ -378,7 +509,9 @@
"bounceInfo": "Notification d'email non distribué",
"underQuotaInfo": "La boîte mail {{ mailbox }} est passée sous le quota de {{ quotaPercent }}%",
"overQuotaInfo": "La boîte mail {{ mailbox }} est pleine à {{ quotaPercent }}%",
"quota": "Quota de boîte mail"
"quota": "Quota de boîte mail",
"savedInfo": "Enregistré",
"sentInfo": "Envoyé"
},
"title": "Journal des événements de la messagerie",
"mailFrom": "De",
@@ -400,7 +533,8 @@
"title": "Domaines",
"outbound": "Sortant uniquement",
"stats": "{{ mailboxCount }} adresse(s) de messagerie / utilisation : {{ usage }}",
"testEmailTooltip": "Envoyer un email test"
"testEmailTooltip": "Envoyer un email test",
"inbound": "Entrant et sortant"
},
"testMailDialog": {
"title": "Envoyer un email test pour {{ domain }}",
@@ -494,7 +628,12 @@
"setupAction": "Créer un compte",
"description": "Un compte Cloudron.io permet d'accéder à l'App Store et de gérer votre abonnement.",
"title": "Compte Cloudron.io",
"emailNotVerified": "Adresse email pas encore confirmée"
"emailNotVerified": "Adresse email pas encore confirmée",
"account": "Compte",
"unlinkAction": "Dissocier le compte",
"unlinkDialog": {
"title": "Désassocier le compte Cloudron.io"
}
},
"registryConfig": {
"provider": "Fournisseur du registre Docker",
@@ -523,11 +662,24 @@
"stopUpdateAction": "Interrompre la mise à jour",
"updateAvailableAction": "Mise à jour disponible",
"checkForUpdatesAction": "Rechercher les mises à jour disponibles",
"title": "Mises à jour"
"title": "Mises à jour",
"disabled": "Désactivé",
"onLatest": "dernier",
"config": "Mises à jour automatiques",
"appsOnly": "Applications uniquement",
"platformAndApps": "Plateforme et applications"
},
"timezone": {
"title": "Fuseau horaire",
"description": "Le fuseau horaire défini actuellement est le suivant : <b>{{ timeZone }}</b>.\nCe paramètre est utilisé pour la planification des opérations de sauvegarde et de mise à jour."
},
"configureUpdates": {
"title": "Configurer les mises à jour automatiques",
"policy": "Stratégie",
"policyDescription": "Choisissez ce qui doit être mis à jour automatiquement",
"days": "Jours",
"hours": "Heures",
"schedule": "Planifier"
}
},
"support": {
@@ -538,7 +690,28 @@
},
"notifications": {
"dismissTooltip": "Supprimer",
"markAllAsRead": "Tout marquer comme lu"
"markAllAsRead": "Tout marquer comme lu",
"settings": {
"title": "Paramètres de notification",
"backupFailed": "Échec de la sauvegarde",
"certificateRenewalFailed": "Échec du renouvellement du certificat",
"appOutOfMemory": "L'application manque de mémoire",
"appUp": "L'application est de nouveau disponible",
"appDown": "L'application est hors service",
"rebootRequired": "Un redémarrage du serveur est nécessaire",
"cloudronUpdateFailed": "Échec de la mise à jour de Cloudron",
"diskSpace": "Espace disque faible",
"appAutoUpdateFailed": "Échec de la mise à jour automatique de l'application",
"manualUpdateRequired": "La plateforme ou l'application nécessite une mise à jour manuelle"
},
"settingsDialog": {
"description": "Un e-mail contenant les événements sélectionnés vous sera envoyé à votre adresse e-mail principale."
},
"title": "Notifications",
"showAll": "Tout",
"showUnread": "Non lu",
"markUnread": "Marquer comme non lu",
"markRead": "Marquer comme lu"
},
"appstore": {
"category": {
@@ -568,10 +741,14 @@
"userManagementLeaveToApp": "Laisser la gestion des utilisateurs à l'application",
"locationPlaceholder": "Laisser vide pour utiliser le nom de domaine nu",
"cloudflarePortWarning": "Le proxy Cloudflare doit être désactivé pour que le domaine de l'application puisse accéder à ce port",
"portReadOnly": "lecture seule"
"portReadOnly": "lecture seule",
"ephemeralPortWarning": "L'utilisation de ports éphémères peut entraîner des conflits imprévisibles."
},
"unstable": "Instable",
"searchPlaceholder": "Rechercher des solutions alternatives telles que GitHub, Dropbox, Slack, Trello…"
"searchPlaceholder": "Rechercher des solutions alternatives telles que GitHub, Dropbox, Slack, Trello…",
"action": {
"addCustomApp": "Ajouter une application personnalisée"
}
},
"app": {
"updatesTabTitle": "Mises à jour",
@@ -581,7 +758,14 @@
"lastUpdated": "Dernière mise à jour",
"packageVersion": "Version du package",
"appId": "ID de l'application",
"description": "Nom et version de l'application"
"description": "Nom et version de l'application",
"installedAt": "Installé"
},
"auto": {
"title": "Mises à jour automatiques"
},
"updates": {
"description": "Cloudron vérifie automatiquement si des mises à jour sont disponibles pour les applications. Vous pouvez également les vérifier manuellement."
}
},
"backupsTabTitle": "Sauvegardes",
@@ -609,10 +793,27 @@
"csp": {
"saveAction": "Enregistrer",
"description": "Le paramétrage de cette option écrasera tous les en-têtes CSP générés par l'application elle-même.",
"title": "Politique de sécurité du contenu (CSP)"
"title": "Politique de sécurité du contenu (CSP)",
"insertCommonCsp": "Insérer un CSP standard",
"commonPattern": {
"allowEmbedding": "Autoriser l'intégration",
"sameOriginEmbedding": "Autoriser l'intégration (uniquement les sous-domaines)",
"allowCdnAssets": "Autoriser les ressources CDN",
"reportOnly": "Signaler les violations du CSP",
"strictBaseline": "Référence stricte"
}
},
"robots": {
"title": "Robots.txt"
"title": "Robots.txt",
"description": "Par défaut, les robots peuvent indexer cette application",
"commonPattern": {
"allowAll": "Tout autoriser (par défaut)",
"disallowAll": "Tout interdire",
"disallowCommonBots": "Bloquer les robots courants",
"disallowAdminPaths": "Interdire les chemins d'accès à l'administration",
"disallowApiPaths": "Interdire les chemins d'accès à l'API"
},
"insertCommonRobotsTxt": "Insérer un fichier robots.txt standard"
},
"hstsPreload": "Activer HSTS pour ce site et tous les sous-domaines"
},
@@ -642,18 +843,27 @@
"operators": {
"title": "Opérateurs",
"description": "Les opérateurs peuvent configurer et assurer la maintenance de cette application."
},
"dashboardVisibility": {
"description": "Définissez qui peut voir cette application sur le tableau de bord."
}
},
"repair": {
"recovery": {
"description": "Si l'application ne répond pas, essayez de redémarrer l'application. Si l'application redémarre sans arrêt à cause d'un plugin défectueux ou d'une anomalie de paramétrage, mettez l'application en mode récupération pour avoir accès à la console. \nSuivez les <a href=\"{{ docsLink }}\" target=\"_blank\">instructions suivantes</a> pour faire fonctionner l'application à nouveau.",
"restartAction": "Redémarrer l'application",
"title": "Récupération après un crash"
"title": "Récupération après un crash",
"disableAction": "Désactiver le mode de récupération",
"enableAction": "Activer le mode de récupération"
},
"taskError": {
"retryAction": "Relancer l'opération {{ task }}",
"description": "Si une action de paramétrage, de mise à jour, de restauration ou de sauvegarde échoue, vous pouvez relancer l'opération.",
"title": "Erreur de tâche"
},
"restart": {
"title": "Redémarrer",
"description": "Si l'application ne répond pas, essayez de la redémarrer."
}
},
"email": {
@@ -685,13 +895,18 @@
"warning": "Toutes les données créées depuis la dernière sauvegarde connue seront définitivement perdues. Il est fortement recommandé de sauvegarder les données actuelles avant de lancer une restauration.",
"description": "Cette action entraînera la restauration de l'application à partir des données de {{ creationTime }}.",
"title": "Restaurer {{ app }}",
"restoreAction": "Restaurer"
"restoreAction": "Restaurer",
"cloneAction": "Cloner",
"cloneActionOverwrite": "Cloner et écraser le DNS"
},
"importBackupDialog": {
"uploadAction": "Charger le fichier de configuration de la sauvegarde",
"title": "Importer la sauvegarde",
"importAction": "Importer",
"remotePath": "Chemin de la sauvegarde"
"remotePath": "Chemin de la sauvegarde",
"provideBackupInfo": "Indiquez les informations de sauvegarde à partir desquelles effectuer la restauration, ou",
"warning": "Toutes les données créées depuis la dernière sauvegarde seront définitivement perdues. Il est recommandé de créer une nouvelle sauvegarde avant l'importation.",
"versionMustMatchInfo": "La sauvegarde doit avoir été créée à l'aide de la même version du package et des mêmes paramètres de contrôle d'accès que cette application."
},
"repairTabTitle": "Réparation",
"uninstallDialog": {
@@ -701,7 +916,10 @@
},
"appInfo": {
"package": "Package",
"openAction": "Ouvrir {{ app }}"
"openAction": "Ouvrir {{ app }}",
"checklist": "Liste de contrôle pour l'administrateur",
"checklistShow": "Afficher la liste de contrôle",
"checklistHide": "Cacher la liste de contrôle"
},
"firstTimeSetupAction": "Initialisation",
"uninstall": {
@@ -728,7 +946,8 @@
"downloadConfigTooltip": "Télécharger le fichier de configuration de la sauvegarde",
"description": "Les sauvegardes sont des instantanés complets de l'application. Vous pouvez utiliser les sauvegardes pour restaurer ou cloner l'application.",
"title": "Sauvegardes",
"downloadBackupTooltip": "Télécharger la sauvegarde"
"downloadBackupTooltip": "Télécharger la sauvegarde",
"checkIntegrity": "Vérifier l'intégrité"
}
},
"graphs": {
@@ -737,7 +956,9 @@
"7d": "7 jours",
"24h": "24 heures",
"12h": "12 heures",
"6h": "6 heures"
"6h": "6 heures",
"live": "En direct",
"1h": "1 heure"
},
"diskIOTotal": "total: lecture {{ read }} / écriture {{ write }}",
"networkIOTotal": "total: entrant {{ inbound }} / sortant {{ outbound }}"
@@ -752,6 +973,10 @@
"description": "Taux limite d'utilisation du microprocesseur lorsque le système est très sollicité.",
"title": "Utilisation du microprocesseur",
"setAction": "Valider"
},
"devices": {
"label": "Appareils",
"description": "Liste des appareils connectés à l'application, séparés par des virgules"
}
},
"location": {
@@ -821,10 +1046,42 @@
},
"servicesTabTitle": "Services",
"turn": {
"title": "Configuration de TURN"
"title": "Configuration de TURN",
"info": "Utilisez le serveur TURN intégré. Si cette option est désactivée, les paramètres TURN de l'application restent inchangés."
},
"redis": {
"title": "Configuration de Redis"
"title": "Configuration de Redis",
"info": "Utilisez le service Redis intégré. Si cette option est désactivée, les paramètres Redis de l'application restent inchangés."
},
"infoTabTitle": "Informations",
"info": {
"notes": {
"title": "Notes de l'administrateur"
}
},
"archive": {
"title": "Archives",
"action": "Archives",
"noBackup": "Cette application ne dispose pas de sauvegarde. L'archivage nécessite une sauvegarde récente."
},
"archiveDialog": {
"title": "Application d'archivage"
},
"updateAvailableTooltip": "Mise à jour disponible",
"configureTooltip": "Configurer",
"forumAction": "Forum",
"appLink": {
"title": "Lien externe"
},
"start": {
"title": "Démarrer",
"description": "Lancez l'application pour qu'elle soit à nouveau disponible.",
"action": "Démarrer"
},
"stop": {
"action": "Arrêter",
"title": "Arrêter",
"description": "Fermez l'application pour économiser les ressources. Sauvegardez vos données avant de fermer l'application afin de conserver les modifications récentes."
}
},
"logs": {
@@ -836,7 +1093,8 @@
"name": "Nom",
"description": "Les volumes sont des systèmes de fichiers locaux ou distants. Ils peuvent être utilisés comme stockage de données principal d'une application ou comme emplacement de stockage partagé entre les applications.",
"removeVolumeDialog": {
"removeAction": "Supprimer"
"removeAction": "Supprimer",
"title": "Supprimer le volume"
},
"addVolumeDialog": {
"title": "Ajouter un volume",
@@ -863,7 +1121,9 @@
"description": "Le texte ci-dessous s'affichera dans tous les emails sortants de ce domaine.",
"plainTextFormat": "Format texte",
"htmlFormat": "Format HTML (optionnel)",
"title": "Signature"
"title": "Signature",
"customSignatureSet": "Signature personnalisée configurée",
"noSignatureSet": "Aucune signature configurée"
},
"incoming": {
"catchall": {
@@ -876,7 +1136,9 @@
"title": "Listes de diffusion",
"name": "Nom",
"everyoneTooltip": "Utilisation de la liste autorisée aux non-membres",
"membersOnlyTooltip": "Utilisation de la liste limitée à ses membres"
"membersOnlyTooltip": "Utilisation de la liste limitée à ses membres",
"emptyPlaceholder": "Pas de listes de diffusion",
"noMatchesPlaceholder": "Aucune liste de diffusion correspondante"
},
"mailboxes": {
"usage": "Utilisation",
@@ -884,7 +1146,9 @@
"title": "Messageries",
"owner": "Propriétaire",
"name": "Nom",
"addAction": "Ajouter"
"addAction": "Ajouter",
"emptyPlaceholder": "Pas de boîtes aux lettres",
"noMatchesPlaceholder": "Aucune boîte aux lettres correspondante"
},
"sieveServerInfo": "ManageSieve",
"incomingServerInfo": "Réception (IMAP)",
@@ -895,7 +1159,8 @@
"howToConnectDescription": "Utilisez les paramètres ci-dessous pour configurer les clients de messagerie.",
"incomingUserInfo": "Identifiant",
"incomingPasswordInfo": "Mot de passe",
"incomingPasswordUsage": "Mot de passe du propriétaire de la boîte mail"
"incomingPasswordUsage": "Mot de passe du propriétaire de la boîte mail",
"description": "Recevoir les e-mails entrants pour ce domaine"
},
"addMailinglistDialog": {
"members": "Liste des membres",
@@ -911,7 +1176,8 @@
},
"addMailboxDialog": {
"title": "Ajouter une adresse de messagerie",
"name": "Nom"
"name": "Nom",
"incomingDisabledWarning": "La réception des e-mails pour ce domaine n'est pas activée"
},
"editMailboxDialog": {
"title": "Paramétrer l'adresse de messagerie {{ name }}@{{ domain }}",
@@ -937,7 +1203,9 @@
},
"smtpStatus": {
"notBlacklisted": "L'adresse IP de ce serveur {{ ip }} <b>n'est pas</b> sur liste noire.",
"blacklisted": "L'adresse IP de ce serveur {{ ip }} est sur liste noire."
"blacklisted": "L'adresse IP de ce serveur {{ ip }} est sur liste noire.",
"outboundSmtp": "SMTP sortant",
"rblCheck": "Vérification de la liste noire DNS"
},
"dnsStatus": {
"recordNotSet": "non défini",
@@ -972,7 +1240,13 @@
},
"config": {
"title": "Configuration de la messagerie {{ domain }}",
"clientConfiguration": "Configuration des clients de messagerie"
"clientConfiguration": "Configuration des clients de messagerie",
"sending": {
"title": "Envoi"
},
"receiving": {
"title": "Réception"
}
},
"editMailinglistDialog": {
"title": "Modifier la liste de diffusion {{ name }}@{{ domain }}"
@@ -984,7 +1258,11 @@
"enablePop3": "Activer l'accès POP3",
"activeCheckbox": "L'adresse de messagerie est active"
},
"howToConnectInfoModal": "Configuration des clients de messagerie"
"howToConnectInfoModal": "Configuration des clients de messagerie",
"customFrom": {
"title": "Autoriser les adresses d'expéditeur personnalisées",
"description": "Autoriser les utilisateurs et les applications authentifiés à utiliser n'importe quelle adresse d'expéditeur"
}
},
"domains": {
"syncDns": {
@@ -1040,12 +1318,24 @@
"bunnyAccessKey": "Bunny Access Key",
"ovhConsumerKey": "Consumer Key",
"ovhAppKey": "Application Key",
"ovhAppSecret": "Application Secret"
"ovhAppSecret": "Application Secret",
"deSecToken": "jeton deSEC",
"gandiTokenType": "Type de jeton",
"gandiTokenTypeApiKey": "Clé API (obsolète)",
"gandiTokenTypePAT": "Jeton d'accès personnel (PAT)",
"inwxUsername": "Nom d'utilisateur INWX",
"inwxPassword": "Mot de passe INWX",
"customNameservers": "Le domaine utilise des serveurs de noms personnalisés (vanity)",
"zoneNamePlaceholder": "Facultatif. Si ce paramètre n'est pas fourni, la valeur par défaut est le domaine racine.",
"carddavLocation": "Emplacement du serveur CardDAV",
"caldavLocation": "Emplacement du serveur CalDAV"
},
"changeDashboardDomain": {
"description": "Cette action entraînera le déplacement du tableau de bord vers le sous-domaine <code>my</code> du domaine sélectionné.",
"changeAction": "Changer le domaine",
"title": "Changer le domaine du tableau de bord"
"title": "Changer le domaine du tableau de bord",
"confirmMessage": "Cela invalidera toutes les clés d'accès des utilisateurs.",
"confirmTitle": "Voulez-vous vraiment modifier le domaine du tableau de bord?"
},
"removeDialog": {
"removeAction": "Supprimer",
@@ -1058,7 +1348,14 @@
},
"provider": "Fournisseur",
"domain": "Domaine",
"title": "Domaines et Certificats"
"title": "Domaines et Certificats",
"emptyPlaceholder": "Aucun domaine",
"noMatchesPlaceholder": "Aucun domaine correspondant",
"description": "L'ajout d'un domaine vous permet d'installer des applications sur ses sous-domaines.",
"wellknown": {
"editAction": "URI courants",
"title": "URI courants"
}
},
"branding": {
"footer": {
@@ -1066,7 +1363,8 @@
},
"title": "Affichage",
"cloudronName": "Nom du Cloudron",
"logo": "Logo"
"logo": "Logo",
"backgroundImage": "Arrière-plan de la page de connexion"
},
"passwordResetEmail": {
"subject": "Réinitialisation du mot de passe [<%= cloudron %>]",
@@ -1115,7 +1413,8 @@
"new": "Nouveau",
"uploadFolder": "Charger un dossier",
"openTerminal": "Ouvrir le terminal",
"openLogs": "Afficher les journaux"
"openLogs": "Afficher les journaux",
"refresh": "Actualiser"
},
"renameDialog": {
"reallyOverwrite": "Un fichier portant ce nom existe déjà. Écraser le fichier existant?",
@@ -1209,7 +1508,9 @@
"downloadAction": "Télécharger",
"scheduler": "Planificateur/Cron",
"download": {
"download": "Télécharger"
"download": "Télécharger",
"title": "Télécharger le fichier",
"description": "Indiquez le chemin d'accès d'un fichier ou d'un répertoire à télécharger depuis le système de fichiers de l'application."
},
"title": "Terminal"
},
@@ -1235,10 +1536,19 @@
"product": "Produit",
"memory": "Mémoire",
"uptime": "Durée de fonctionnement",
"activationTime": "Heure de création de Cloudron"
"activationTime": "Heure de création de Cloudron",
"cloudronVersion": "Version de Cloudron",
"ubuntuVersion": "Version de Ubuntu"
},
"graphs": {
"title": "Graphiques"
},
"locale": {
"title": "Paramètres régionaux"
},
"title": "Système",
"settings": {
"title": "Paramètres"
}
},
"services": {
@@ -1274,7 +1584,8 @@
"noUsername": {
"title": "Impossible de configurer le compte",
"description": "Le compte ne peut pas être configuré sans nom d'utilisateur."
}
},
"welcome": "Bienvenue"
},
"login": {
"resetPasswordAction": "Réinitialiser le mot de passe",
@@ -1283,7 +1594,11 @@
"username": "Nom d'utilisateur",
"errorIncorrectCredentials": "Nom d'utilisateur ou mot de passe incorrect",
"errorIncorrect2FAToken": "Le jeton 2FA n'est pas valide",
"errorInternal": "Erreur interne, réessayer ultérieurement"
"errorInternal": "Erreur interne, réessayer ultérieurement",
"loginAction": "Se connecter",
"usePasskeyAction": "Utiliser une clé d'accès",
"errorPasskeyFailed": "Échec de la connexion avec la clé d'accès",
"passkeyAction": "Se connecter avec la clé d'accès"
},
"newLoginEmail": {
"salutation": "Bonjour <%= user %>,",
@@ -1303,7 +1618,8 @@
"name": "Nom",
"id": "ID du client",
"secret": "Secret du client",
"loginRedirectUri": "Url de retour post connexion (séparées par des virgules s'il y en a plus d'une)"
"loginRedirectUri": "Url de retour post connexion (séparées par des virgules s'il y en a plus d'une)",
"loginRedirectUriPlaceholder": "URL séparées par des virgules"
},
"description": "Cloudron peut agir en tant que fournisseur OpenID Connect pour les applications internes et les services externes.",
"deleteClientDialog": {
@@ -1319,6 +1635,73 @@
},
"env": {
"discoveryUrl": "URL de découverte"
},
"clients": {
"title": "Clients OpenID",
"empty": "Aucun client OpenID"
},
"clientCredentials": {
"title": "Identifiants du client"
}
},
"userdirectory": {
"settings": {
"title": "Paramètres"
}
},
"archives": {
"listing": {
"placeholder": "Aucune application archivée"
},
"description": "Les applications archivées conservent la dernière sauvegarde effectuée au moment de leur archivage. Ces sauvegardes sont conservées de manière permanente et peuvent être restaurées."
},
"backup": {
"target": {
"label": "Site",
"size": "Taille",
"fileCount": "Fichiers"
},
"sites": {
"title": "Sites de secours",
"emptyPlaceholder": "Pas de sites de secours",
"lastRun": "Dernier lancement",
"description": "Les emplacements de sauvegarde indiquent où sont stockées les sauvegardes du système et des applications. Les sauvegardes des applications peuvent être restaurées individuellement.",
"noAutomaticUpdateBackupWarning": "Aucun site de sauvegarde n'est configuré pour stocker les sauvegardes des mises à jour automatiques. Activez l'option « Stocker ici les sauvegardes des mises à jour automatiques » sur au moins un site de sauvegarde pour permettre les mises à jour automatiques."
},
"site": {
"removeDialog": {
"title": "Supprimer le site de secours"
}
}
},
"dockerRegistries": {
"server": "Adresse du serveur",
"provider": "Fournisseur",
"username": "Nom d'utilisateur",
"title": "Registres Docker",
"description": "Configurer l'accès aux registres Docker privés pour l'installation d'applications personnalisées.",
"removeDialog": {
"title": "Supprimer le registre Docker"
},
"email": "E-mail",
"passwordToken": "Mot de passe/Jeton",
"emptyPlaceholder": "Pas de registres Docker",
"dialog": {
"addTitle": "Ajouter un registre Docker",
"editTitle": "Modifier le registre Docker"
}
},
"appearance": {
"title": "Apparence"
},
"dashboard": {
"title": "Tableau de bord"
},
"server": {
"title": "Serveur"
},
"communityapp": {
"installwarning": "Les applications de la communauté ne sont pas vérifiées par Cloudron. N'installez que des applications provenant de développeurs de confiance. Le code tiers peut compromettre la sécurité de votre système.",
"unstablewarning": "Cette application est signalée comme instable par son développeur."
}
}

View File

@@ -44,7 +44,9 @@
"restart": "Mulai ulang",
"reset": "Atur Ulang",
"logs": "Log",
"loadMore": "Muat lebih banyak"
"loadMore": "Muat lebih banyak",
"setup": "Siapkan",
"disable": "Nonaktifkan"
},
"searchPlaceholder": "Cari",
"actions": "Tindakan",
@@ -361,8 +363,23 @@
},
"twoFactorAuth": {
"title": "Autentikasi dua faktor",
"totpEnabled": "Menggunakan kata sandi sekali pakai berbasis waktu (TOTP)",
"passkeyEnabled": "Menggunakan passkey"
"totpEnabled": "Diaktifkan",
"passkeyEnabled": "Diaktifkan",
"totpTitle": "TOTP",
"passkeyTitle": "Passkey"
},
"notSet": "Belum diatur",
"enablePasskey": {
"title": "Aktifkan passkey"
},
"enableTotp": {
"title": "Aktifkan TOTP"
},
"disableTotp": {
"title": "Nonaktifkan TOTP"
},
"disablePasskey": {
"title": "Nonaktifkan Passkey"
}
},
"backups": {
@@ -724,8 +741,11 @@
"updateAvailableAction": "Pembaruan tersedia",
"stopUpdateAction": "Hentikan pembaruan",
"disabled": "Dinonaktifkan",
"description": "Pembaruan platform dan aplikasi diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan <a href=\"/#/system-settings\">Zona waktu sistem</a>.",
"onLatest": "terbaru"
"description": "Pembaruan diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan <a href=\"/#/system-settings\">Zona waktu sistem</a>.",
"onLatest": "terbaru",
"config": "Pembaruan otomatis",
"appsOnly": "Hanya aplikasi",
"platformAndApps": "Platform & aplikasi"
},
"updateScheduleDialog": {
"disableCheckbox": "Nonaktifkan pembaruan otomatis",
@@ -750,6 +770,14 @@
"registryConfig": {
"provider": "Penyedia registri Docker",
"providerOther": "Lainnya"
},
"configureUpdates": {
"title": "Konfigurasi Pembaruan Otomatis",
"policy": "Kebijakan",
"policyDescription": "Pilih apa yang diperbarui secara otomatis",
"days": "Hari",
"hours": "Jam",
"schedule": "Jadwal"
}
},
"support": {
@@ -1180,7 +1208,7 @@
"aliases": "Alias",
"addAliasAction": "Tambahkan alias",
"noAliases": "Tidak ada domain alias",
"dnsoverwrite": "Beberapa catatan DNS sudah ada. Setuju untuk menimpa."
"overwriteDns": "Menimpa catatan DNS yang ada pada {domains}"
},
"accessControl": {
"userManagement": {
@@ -1662,7 +1690,9 @@
"appDown": "Aplikasi sedang tidak berfungsi",
"rebootRequired": "Diperlukan menyalakan ulang server",
"cloudronUpdateFailed": "Pembaruan Cloudron gagal",
"diskSpace": "Ruang disk hampir penuh"
"diskSpace": "Ruang disk hampir penuh",
"appAutoUpdateFailed": "Pembaruan otomatis aplikasi gagal",
"manualUpdateRequired": "Platform atau aplikasi memerlukan pembaruan manual"
},
"settingsDialog": {
"description": "E-mail akan dikirimkan ke e-mail utama Anda untuk acara-acara yang dipilih."
@@ -1688,7 +1718,9 @@
"errorIncorrect2FAToken": "Token 2FA tidak valid",
"errorInternal": "Terjadi kesalahan internal, coba lagi nanti",
"loginAction": "Masuk",
"usePasskeyAction": "Gunakan passkey"
"usePasskeyAction": "Gunakan passkey",
"errorPasskeyFailed": "Gagal masuk dengan passkey",
"passkeyAction": "Masuk dengan passkey"
},
"passwordReset": {
"title": "Pengaturan ulang kata sandi",

View File

@@ -47,7 +47,9 @@
"configure": "Configureer",
"restart": "Herstart",
"reset": "Reset",
"loadMore": "Laad meer"
"loadMore": "Laad meer",
"setup": "Instellen",
"disable": "Uitschakelen"
},
"rebootDialog": {
"title": "Herstart Server",
@@ -361,8 +363,23 @@
},
"twoFactorAuth": {
"title": "Twee-Factor (2FA) authenticatie",
"totpEnabled": "Gebruikt tijdgebaseerd eenmalige wachtwoord (TOTP)",
"passkeyEnabled": "Gebruikt passkey"
"totpEnabled": "Ingeschakeld",
"passkeyEnabled": "Ingeschakeld",
"totpTitle": "TOTP",
"passkeyTitle": "Passkey"
},
"notSet": "Niet ingesteld",
"enablePasskey": {
"title": "Passkey activeren"
},
"enableTotp": {
"title": "TOTP activeren"
},
"disableTotp": {
"title": "TOTP Uitschakelen"
},
"disablePasskey": {
"title": "Passkey uitschakelen"
}
},
"backups": {
@@ -769,7 +786,7 @@
"noAliases": "Geen alias-domeinen",
"addAliasAction": "Alias toevoegen",
"aliases": "Aliassen",
"dnsoverwrite": "Sommige DNS records bestaan al. Weet je zeker dat ze overschreven moeten worden?"
"overwriteDns": "Overschrijf bestaande DNS records van {domains}"
},
"accessControl": {
"userManagement": {
@@ -1141,9 +1158,12 @@
"checkForUpdatesAction": "Controleer op updates",
"updateAvailableAction": "Update beschikbaar",
"stopUpdateAction": "Stop update",
"description": "Platform en app updates worden toegepast met de geconfigureerde planning met deze <a href=\"/#/system-locale\">Systeem tijdzone</a>.",
"description": "Updates worden toegepast volgens het geconfigureerde schema, met behulp van de <a href=\"/#/system-settings\">System time zone</a>.",
"disabled": "Uitgeschakeld",
"onLatest": "Laatste"
"onLatest": "Laatste",
"config": "Automatische updates",
"appsOnly": "Alleen Apps",
"platformAndApps": "Platform & Apps"
},
"updateScheduleDialog": {
"disableCheckbox": "Automatische updates uitschakelen",
@@ -1169,6 +1189,14 @@
"registryConfig": {
"provider": "Docker registry aanbieder",
"providerOther": "Anders"
},
"configureUpdates": {
"title": "Automatische updates configureren",
"policy": "Beleid",
"policyDescription": "Kies wat er automatisch wordt bijgewerkt",
"days": "Dagen",
"hours": "Uren",
"schedule": "Planning"
}
},
"support": {
@@ -1226,7 +1254,9 @@
"appDown": "App werkt niet",
"rebootRequired": "Server herstart noodzakelijk",
"cloudronUpdateFailed": "Cloudron update mislukt",
"diskSpace": "Weinig diskruimte"
"diskSpace": "Weinig diskruimte",
"appAutoUpdateFailed": "Automatische update van de app is mislukt",
"manualUpdateRequired": "Platform of app moet handmatig geüpdatet worden"
},
"settingsDialog": {
"description": "Een e-mail wordt verstuurd voor de geselecteerde gebeurtenissen naar je primaire e-mail."
@@ -1513,7 +1543,9 @@
"errorIncorrect2FAToken": "2FA token is niet geldig",
"errorInternal": "Interne fout, probeer later opnieuw",
"loginAction": "Inloggen",
"usePasskeyAction": "Gebruik een passkey"
"usePasskeyAction": "Gebruik een passkey",
"errorPasskeyFailed": "Inloggen met passkey mislukt",
"passkeyAction": "Inloggen met een passkey"
},
"passwordReset": {
"title": "Wachtwoord herstellen",

View File

@@ -419,8 +419,7 @@
"saveAction": "Сохранить",
"aliases": "Псевдонимы",
"addAliasAction": "Добавить псевдоним",
"noAliases": "Домены-псевдонимы отсутствуют",
"dnsoverwrite": "Некоторые DNS записи уже существуют. Подтвердите перезапись."
"noAliases": "Домены-псевдонимы отсутствуют"
},
"accessControl": {
"sftp": {

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ import ProvisionModel from './models/ProvisionModel.js';
import NotificationsModel from './models/NotificationsModel.js';
import DashboardModel from './models/DashboardModel.js';
import BrandingModel from './models/BrandingModel.js';
import AppstoreModel from './models/AppstoreModel.js';
import Headerbar from './components/Headerbar.vue';
import SubscriptionRequiredDialog from './components/SubscriptionRequiredDialog.vue';
import RequestErrorDialog from './components/RequestErrorDialog.vue';
@@ -277,6 +278,7 @@ const dashboardModel = DashboardModel.create();
const profileModel = ProfileModel.create();
const provisionModel = ProvisionModel.create();
const notificationModel = NotificationsModel.create();
const appstoreModel = AppstoreModel.create();
const inputDialog = useTemplateRef('inputDialog');
const subscriptionRequiredDialog = useTemplateRef('subscriptionRequiredDialog');
@@ -405,6 +407,14 @@ async function refreshNotifications() {
notificationCount.value = result.length;
}
async function refreshSubscription() {
const [error, result] = await appstoreModel.getSubscription();
if (error && error.status === 402) console.error('Not yet registered');
else if (error && error.status === 412) window.location.href = ''
else if (error) console.error(error);
else subscription.value = result;
}
async function onOnline() {
ready.value = true;
await refreshConfigAndFeatures(); // reload dashboard if needed after an update
@@ -416,6 +426,7 @@ function checkForMobile() {
}
provide('subscriptionRequiredDialog', subscriptionRequiredDialog);
provide('subscription', subscription);
provide('features', features);
provide('profile', profile);
provide('refreshProfile', refreshProfile);
@@ -455,7 +466,10 @@ onMounted(async () => {
console.log(`Cloudron dashboard v${config.value.version}`);
if (profile.value.isAtLeastAdmin) refreshNotifications();
if (profile.value.isAtLeastAdmin) {
refreshNotifications();
refreshSubscription();
}
ready.value = true;
@@ -483,7 +497,7 @@ onUnmounted(() => {
<SideBar v-if="profile.isAtLeastUserManager" :items="menuItems" :cloudron-name="config.cloudronName" :cloudron-avatar-url="avatarUrl"/>
<div style="flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; height: 100%;">
<Headerbar :config="config" :subscription="subscription" :notification-count="notificationCount"/>
<Headerbar :config="config" :notification-count="notificationCount"/>
<div style="display: flex; justify-content: center; overflow: auto; flex-grow: 1; padding: 0; margin: 0 10px; position: relative;">
<KeepAlive>

View File

@@ -16,6 +16,7 @@ import TokensModel from '../models/TokensModel.js';
const tokensModel = TokensModel.create();
const apiTokens = ref([]);
const loading = ref(true);
const inputDialog = useTemplateRef('inputDialog');
const newDialog = useTemplateRef('newDialog');
const addedToken = ref('');
@@ -122,6 +123,7 @@ async function onRevokeToken(apiToken) {
onMounted(async () => {
await refreshApiTokens();
loading.value = false;
});
</script>
@@ -184,7 +186,7 @@ onMounted(async () => {
<div v-html="$t('profile.apiTokens.description', { apiDocsLink: 'https://docs.cloudron.io/api.html' })"></div>
<br/>
<TableView :columns="columns" :model="apiTokens" :placeholder="$t('profile.apiTokens.noTokensPlaceholder')">
<TableView :columns="columns" :model="apiTokens" :busy="loading" :placeholder="$t('profile.apiTokens.noTokensPlaceholder')">
<template #lastUsedTime="{ item:apiToken }">
<span v-if="apiToken.lastUsedTime">{{ prettyLongDate(apiToken.lastUsedTime) }}</span>
<span v-else>{{ $t('profile.apiTokens.neverUsed') }}</span>

View File

@@ -2,7 +2,7 @@
import { ref, computed, useTemplateRef, onMounted, inject, watch } from 'vue';
import { marked } from 'marked';
import { Button, Dialog, SingleSelect, FormGroup, TextInput, InputGroup, Spinner } from '@cloudron/pankow';
import { Button, Checkbox, Dialog, SingleSelect, FormGroup, TextInput, InputGroup, Spinner } from '@cloudron/pankow';
import { prettyDate, prettyBinarySize } from '@cloudron/pankow/utils';
import AccessControl from './AccessControl.vue';
import PortBindings from './PortBindings.vue';
@@ -42,6 +42,12 @@ const domains = ref([]);
const form = ref(null); // assigned via "Function Ref" because it is inside v-if
const isFormValid = ref(false);
function resetDnsOverwrite() {
needsOverwriteDns.value = [];
overwriteDns.value = false;
formError.value = {};
}
async function checkValidity() {
isFormValid.value = form.value ? form.value.checkValidity() : false;
@@ -89,7 +95,8 @@ const tcpPorts = ref({});
const udpPorts = ref({});
const secondaryDomains = ref({});
const upstreamUri = ref('');
const needsOverwriteDns = ref(false);
const overwriteDns = ref(false);
const needsOverwriteDns = ref([]);
const users = ref([]);
const groups = ref([]);
@@ -98,7 +105,7 @@ function onDomainChange() {
domainProvider.value = tmp ? tmp.provider : '';
}
async function onSubmit(overwriteDns) {
async function onSubmit() {
if (!form.value.reportValidity()) return;
formError.value = {};
@@ -111,6 +118,7 @@ async function onSubmit(overwriteDns) {
for (const d in secondaryDomains.value) checkForDomains.push({ domain: secondaryDomains.value[d].domain, subdomain: secondaryDomains.value[d].value });
const conflicting = [];
for (const d of checkForDomains) {
const [error, result] = await domainsModel.checkRecords(d.domain, d.subdomain);
if (error) {
@@ -119,12 +127,14 @@ async function onSubmit(overwriteDns) {
return console.error(error);
}
if (result.needsOverwrite && !overwriteDns) {
busy.value = false;
needsOverwriteDns.value = true;
formError.value.dnsExists = `DNS record for ${d.subdomain}.${d.domain} already exists`;
return;
}
if (result.needsOverwrite) conflicting.push((d.subdomain ? d.subdomain + '.' : '') + d.domain);
}
if (conflicting.length > 0 && !overwriteDns.value) {
busy.value = false;
needsOverwriteDns.value = conflicting;
formError.value.generic = `DNS records of ${conflicting.join(', ')} already exist`;
return;
}
const config = {
@@ -133,7 +143,7 @@ async function onSubmit(overwriteDns) {
accessRestriction: accessRestrictionOption.value === ACL_OPTIONS.ANY ? null : (accessRestrictionOption.value === ACL_OPTIONS.NOSSO ? null : accessRestrictionAcl.value)
};
if (overwriteDns) config.overwriteDns = true;
if (overwriteDns.value) config.overwriteDns = true;
if (manifest.value.optionalSso) config.sso = accessRestrictionOption.value !== ACL_OPTIONS.NOSSO;
@@ -185,7 +195,7 @@ function onClose() {
onMounted(async () => {
let [error, result] = await usersModel.list();
if (error) return console.error(error);
result.forEach(u => { u.label = u.displayName || u.username || u.email });
result.forEach(u => { u.label = u.displayName || u.username || u.email; });
users.value = result;
[error, result] = await groupsModel.list();
@@ -225,7 +235,8 @@ defineExpose({
accessRestrictionAcl.value = { users: [], groups: [] };
domainProvider.value = '';
upstreamUri.value = '';
needsOverwriteDns.value = '';
overwriteDns.value = false;
needsOverwriteDns.value = [];
domainList.forEach(d => {
d.label = '.' + d.domain;
@@ -296,18 +307,15 @@ defineExpose({
<div class="description" v-html="description"></div>
</div>
<div v-else-if="step === STEP.INSTALL">
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
<div class="error-label" v-if="formError.dnsExists">{{ formError.dnsExists }}</div>
<form :ref="(el) => { form = el; }" @submit.prevent="onSubmit(false)" autocomplete="off" @input="checkValidity()">
<form :ref="(el) => { form = el; }" @submit.prevent="onSubmit()" autocomplete="off" @input="checkValidity()">
<fieldset :disabled="busy">
<input style="display: none;" type="submit" :disabled="busy" />
<FormGroup :class="{ 'has-error': formError.location }">
<label for="location">{{ $t('appstore.installDialog.location') }}</label>
<InputGroup>
<TextInput id="location" ref="locationInput" v-model="location" style="flex-grow: 1"/>
<SingleSelect v-model="domain" :options="domains" option-label="label" option-key="domain" @select="onDomainChange()" :search-threshold="10" required/>
<TextInput id="location" ref="locationInput" v-model="location" @input="resetDnsOverwrite()" style="flex-grow: 1"/>
<SingleSelect v-model="domain" :options="domains" option-label="label" option-key="domain" @select="onDomainChange(); resetDnsOverwrite()" :search-threshold="10" required/>
</InputGroup>
<div class="warning-label" v-show="domainProvider === 'noop' || domainProvider === 'manual'" v-html="$t('appstore.installDialog.manualWarning', { location: ((location ? location + '.' : '') + domain) })"></div>
<div class="error-label" v-if="formError.location">{{ formError.location }}</div>
@@ -317,8 +325,8 @@ defineExpose({
<label :for="'secondaryDomainInput' + key">{{ port.title }}</label>
<small>{{ port.description }}</small>
<InputGroup>
<TextInput :id="'secondaryDomainInput' + key" v-model="port.value" :placeholder="$t('appstore.installDialog.locationPlaceholder')" style="flex-grow: 1"/>
<SingleSelect v-model="port.domain" :options="domains" option-label="label" option-key="domain" required/>
<TextInput :id="'secondaryDomainInput' + key" v-model="port.value" @input="resetDnsOverwrite()" :placeholder="$t('appstore.installDialog.locationPlaceholder')" style="flex-grow: 1"/>
<SingleSelect v-model="port.domain" :options="domains" option-label="label" option-key="domain" @select="resetDnsOverwrite()" required/>
</InputGroup>
</FormGroup>
@@ -330,9 +338,13 @@ defineExpose({
<PortBindings v-model:tcp="tcpPorts" v-model:udp="udpPorts" :error="formError" :domain-provider="domainProvider"/>
<AccessControl v-model:option="accessRestrictionOption" v-model:acl="accessRestrictionAcl" :manifest="manifest" :users="users" :groups="groups" :installation="true"/>
<br/>
<div class="text-danger" v-if="formError.generic">{{ formError.generic }}</div>
<Checkbox v-if="needsOverwriteDns.length" v-model="overwriteDns" style="margin-top: 10px" :label="$t('app.location.overwriteDns', { domains: needsOverwriteDns.join(', ') })"/>
<div class="bottom-button-bar">
<Button v-if="needsOverwriteDns" danger @click="onSubmit(true)" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid" :loading="busy">Install {{ manifest.title }} and overwrite DNS</Button>
<Button v-else @click="onSubmit(false)" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid" :loading="busy">Install {{ manifest.title }}</Button>
<Button @click="onSubmit()" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid || (needsOverwriteDns.length > 0 && !overwriteDns)" :loading="busy">Install {{ manifest.title }}</Button>
</div>
</fieldset>
</form>

View File

@@ -65,6 +65,7 @@ const identifier = ref('');
const expiresAtDate = ref('');
const minExpiresAt = new Date().toISOString().slice(0, 16);
const addError = ref('');
const loading = ref(true);
const busy = ref(false);
const appsById = {};
@@ -163,7 +164,7 @@ onMounted(async () => {
if (app.manifest.addons.email) return;
const ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
const sso = app.sso && (app.manifest.addons.ldap || app.manifest.addons.proxyAuth);
const sso = app.sso && (app.manifest.addons.ldap || app.manifest.addons.oidc || app.manifest.addons.proxyAuth);
if (!ftp && !sso) return;
@@ -179,6 +180,7 @@ onMounted(async () => {
});
await refresh();
loading.value = false;
});
</script>
@@ -242,7 +244,7 @@ onMounted(async () => {
<div>{{ $t('profile.appPasswords.description') }}</div>
<br/>
<TableView :columns="columns" :model="passwords" :placeholder="$t('profile.appPasswords.noPasswordsPlaceholder')">
<TableView :columns="columns" :model="passwords" :busy="loading" :placeholder="$t('profile.appPasswords.noPasswordsPlaceholder')">
<template #name="{ item:password }"><span :class="{ 'text-muted': password.expired }">{{ password.name }}</span></template>
<template #label="{ item:password }"><span :class="{ 'text-muted': password.expired }">{{ password.label }}</span></template>
<template #creationTime="{ item:password }"><span :class="{ 'text-muted': password.expired }">{{ prettyLongDate(password.creationTime) }}</span></template>

View File

@@ -135,7 +135,7 @@ onMounted(async () => {
<FormGroup v-if="provider === 'mountpoint'">
<label for="mountPointInput">{{ $t('backups.configureBackupStorage.mountPoint') }}</label>
<TextInput id="mountPointInput" v-model="providerConfig.mountPoint" placeholder="/mnt/backups" required/>
<small class="helper-text" v-html="$t('backups.configureBackupStorage.mountPointDescription', { providerDocsLink: `https://docs.cloudron.io/backups/#${provider}` })"></small>
<small class="warning-label" v-html="$t('backups.configureBackupStorage.mountPointDescription', { providerDocsLink: 'https://docs.cloudron.io/backups/#user-managed-mount-point' })"></small>
</FormGroup>
<!-- CIFS/NFS/SSHFS -->
@@ -172,12 +172,6 @@ onMounted(async () => {
<SingleSelect id="blockDevicePath" v-if="provider === 'xfs'" v-model="providerConfig.mountOptionDiskPath" :options="xfsBlockDevices" option-label="label" option-key="path"/>
</FormGroup>
<!-- Disk -->
<FormGroup v-if="provider === 'disk'">
<label class="control-label">{{ $t('backups.configureBackupStorage.diskPath') }}</label>
<TextInput id="mountOptionDiskPathInput" v-model="providerConfig.mountOptionDiskPath" placeholder="/dev/disk/by-uuid/uuid" required />
</FormGroup>
<!-- SSHFS -->
<FormGroup v-if="provider === 'sshfs'">
<label for="mountOptionPortInput">{{ $t('backups.configureBackupStorage.port') }}</label>

View File

@@ -124,7 +124,7 @@ async function onSubmit() {
data.mountOptions.privateKey = providerConfig.value.mountOptionPrivateKey;
data.preserveAttributes = true;
}
} else if (provider.value === 'ext4' || provider.value === 'xfs' || provider.value === 'disk') {
} else if (provider.value === 'ext4' || provider.value === 'xfs') {
data.mountOptions.diskPath = providerConfig.value.mountOptionDiskPath;
data.preserveAttributes = true;
} else if (provider.value === 'mountpoint') {

View File

@@ -10,6 +10,7 @@ const emit = defineEmits([ 'success' ]);
const dialog = useTemplateRef('dialog');
const form = useTemplateRef('form');
const urlInput = useTemplateRef('urlInput');
const formError = ref({});
const versionsUrl = ref('');
@@ -56,6 +57,7 @@ defineExpose({
unstable.value = false;
dialog.value.open();
setTimeout(validateForm, 100); // update state of the confirm button
setTimeout(() => urlInput.value.focus(), 500);
}
});
@@ -81,7 +83,7 @@ defineExpose({
<FormGroup>
<label for="urlInput">CloudronVersions.json URL</label>
<TextInput id="urlInput" v-model="versionsUrl" required placeholder="https://example.com/CloudronVersions.json"/>
<TextInput id="urlInput" ref="urlInput" v-model="versionsUrl" required placeholder="https://example.com/CloudronVersions.json"/>
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
</FormGroup>
</fieldset>

View File

@@ -319,7 +319,7 @@ function onGcdnsFileInputChange(event) {
<Checkbox v-if="showAdvanced" v-model="customNameservers" :label="$t('domains.domainDialog.customNameservers')" />
<FormGroup v-if="showAdvanced">
<label>Certificate provider <sup><a href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label>Certificate provider <sup><a href="https://docs.cloudron.io/domains#certificates" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<SingleSelect v-model="tlsProvider" :options="tlsProviders" option-key="value" option-label="name" required/>
</FormGroup>

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, reactive, computed, onMounted, watch, nextTick, useTemplateRef } from 'vue';
import { ref, computed, onMounted, watch, nextTick, useTemplateRef } from 'vue';
import { Button, TextInput, MultiSelect, Popover, FormGroup, DateTimeInput } from '@cloudron/pankow';
import { useDebouncedRef, prettyLongDate } from '@cloudron/pankow/utils';
import AppsModel from '../models/AppsModel.js';
@@ -21,7 +21,7 @@ const refreshBusy = ref(false);
const page = ref(1);
const perPage = ref(100);
const eventlogContainer = useTemplateRef('eventlogContainer');
const actions = reactive([]);
const actions = ref([]);
const highlight = useDebouncedRef('', 300);
const currentMatchPosition = ref(-1);
@@ -107,7 +107,7 @@ async function goToNextMatch() {
function buildFilter() {
const filter = {};
if (actions.length) filter.actions = actions.join(',');
if (actions.value.length) filter.actions = actions.value.join(',');
if (filterFrom.value) filter.from = new Date(filterFrom.value + 'T00:00:00').toISOString();
if (filterTo.value) filter.to = new Date(filterTo.value + 'T23:59:59.999').toISOString();
return filter;
@@ -142,7 +142,7 @@ function onOpenDateFilter(event) {
dateFilterPopover.value.open(event, dateFilterButton.value.$el || dateFilterButton.value);
}
watch(actions, onRefresh);
watch(actions.value, onRefresh);
watch(filterFrom, onRefresh);
watch(filterTo, onRefresh);
watch(highlight, async () => {
@@ -202,8 +202,8 @@ defineExpose({ refresh: onRefresh, setHighlight });
<table class="eventlog-table">
<thead>
<tr>
<th style="width: 160px;">{{ $t('eventlog.time') }}</th>
<th style="width: 15%;">{{ $t('eventlog.source') }}</th>
<th style="width: 190px;">{{ $t('eventlog.time') }}</th>
<th style="width: 100px;">{{ $t('eventlog.source') }}</th>
<th>{{ $t('eventlog.details') }}</th>
<th style="width: 40px;"></th>
</tr>
@@ -211,9 +211,9 @@ defineExpose({ refresh: onRefresh, setHighlight });
<tbody>
<template v-for="(eventlog, index) in eventlogs" :key="eventlog.id">
<tr :data-index="index" :class="{ 'active': eventlog.isOpen, 'eventlog-match': highlight && isMatch(eventlog, highlight), 'eventlog-match-current': matchIndices[currentMatchPosition] === index }" @click="eventlog.isOpen = !eventlog.isOpen">
<td style="white-space: nowrap;">{{ prettyLongDate(eventlog.raw.creationTime) }}</td>
<td>{{ prettyLongDate(eventlog.raw.creationTime) }}</td>
<td class="eventlog-source">{{ eventlog.source }}</td>
<td style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" v-html="eventlog.details"></td>
<td v-html="eventlog.details"></td>
<td><Button v-if="eventlog.raw.data.taskId" @click.stop plain small tool :href="app ? `/logs.html?appId=${app.id}&taskId=${eventlog.raw.data.taskId}` : `/logs.html?taskId=${eventlog.raw.data.taskId}`" target="_blank">Logs</Button></td>
</tr>
<tr v-show="eventlog.isOpen">
@@ -243,6 +243,9 @@ defineExpose({ refresh: onRefresh, setHighlight });
.eventlog-table th,
.eventlog-table td {
padding: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.eventlog-table tbody tr.active,

View File

@@ -10,9 +10,10 @@ import { Menu, Popover, Icon, InputDialog, Spinner } from '@cloudron/pankow';
import ServicesModel from '../models/ServicesModel.js';
import ProfileModel from '../models/ProfileModel.js';
defineProps(['config', 'subscription', 'notificationCount']);
defineProps(['config', 'notificationCount']);
const profile = inject('profile');
const subscription = inject('subscription');
const helpButton = useTemplateRef('helpButton');
const helpPopover = useTemplateRef('helpPopover');
@@ -115,8 +116,9 @@ onUnmounted(() => {
<Icon :icon="'fa fa-exclamation-triangle'"/> {{ $t('main.platform.startupFailed') }}
</div>
<!-- Warnings if subscription is expired or unpaid -->
<div v-if="profile.isAtLeastOwner && subscription.plan.id === 'expired'" class="headerbar-action subscription-expired" style="gap: 6px" @click="onSubscriptionRequired()">Subscription Expired</div>
<!-- Warnings if subscription is expired, unpaid or canceled -->
<a v-if="profile.isAtLeastOwner && subscription.plan.id === 'expired'" class="headerbar-action subscription-expired" href="/#/cloudron-account">Subscription Expired</a>
<a v-else-if="profile.isAtLeastOwner && (subscription.cancel_at || subscription.status === 'canceled')" class="headerbar-action subscription-canceled" href="/#/cloudron-account">Subscription Canceled</a>
<a class="headerbar-action" v-if="profile.isAtLeastAdmin" href="/#/notifications"><Icon :icon="notificationCount > 0 ? 'fas fa-bell' : 'far fa-bell'"/> {{ notificationCount > 99 ? '99+' : notificationCount }}</a>
<div class="headerbar-action pankow-no-mobile" v-if="profile.isAtLeastAdmin" ref="helpButton" @click="onOpenHelp(helpPopover, $event, helpButton)"><Icon icon="fa fa-question"/></div>
@@ -168,13 +170,16 @@ onUnmounted(() => {
border-bottom: 1px solid var(--pankow-input-border-color);
}
.subscription-expired {
.subscription-expired,
.subscription-canceled {
background-color: var(--pankow-color-danger);
color: white;
border-radius: 20px;
font-size: 11px;
}
.subscription-expired:hover {
.subscription-expired:hover,
.subscription-canceled:hover {
color: white;
background-color: var(--pankow-color-danger-hover);
}

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { Dialog, TextInput, ClipboardButton, FormGroup, Button, InputGroup } from '@cloudron/pankow';
import { Dialog, TextInput, ClipboardButton, FormGroup, InputGroup } from '@cloudron/pankow';
import UsersModel from '../models/UsersModel.js';
const usersModel = UsersModel.create();
@@ -13,17 +13,16 @@ const password = ref('');
const success = ref(false);
const busy = ref(false);
// https://stackoverflow.com/questions/1497481/javascript-password-generator
function onGeneratePassword() {
const length = 12;
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let tmp = '';
for (var i = 0, n = charset.length; i < length; ++i) {
tmp += charset.charAt(Math.floor(Math.random() * n));
function generatePassword() {
const blocks = [];
const values = new Uint8Array(16);
crypto.getRandomValues(values);
for (let b = 0; b < 4; b++) {
let block = '';
for (let i = 0; i < 4; i++) block += String.fromCharCode(97 + (values[b * 4 + i] % 26));
blocks.push(block);
}
password.value = tmp;
return blocks.join('-');
}
async function onSubmit() {
@@ -45,7 +44,7 @@ defineExpose({
u = JSON.parse(JSON.stringify(u)); // make a copy
user.value = u;
success.value = false;
password.value = '';
password.value = generatePassword();
formError.value = '';
dialog.value.open();
@@ -71,9 +70,8 @@ defineExpose({
<FormGroup>
<label for="passwordInput">{{ $t('users.setGhostDialog.password') }}</label>
<InputGroup>
<TextInput id="passwordInput" v-model="password" style="flex-grow: 1;"/>
<ClipboardButton v-if="success" :value="password" />
<Button tool v-else @click="onGeneratePassword()" v-tooltip="$t('users.setGhostDialog.generatePassword')" icon="fa fa-key" />
<TextInput id="passwordInput" v-model="password" readonly style="flex-grow: 1;"/>
<ClipboardButton :value="password" />
</InputGroup>
</FormGroup>
</fieldset>

View File

@@ -209,7 +209,7 @@ body {
color: white;
font-family: monospace;
font-size: 14px;
white-space: nowrap;
white-space: pre-wrap;
width: 100%;
}

View File

@@ -17,6 +17,7 @@ const appAutoUpdateFailed = ref(false);
const certificateRenewalFailed = ref(false);
const diskSpace = ref(false);
const cloudronUpdateFailed = ref(false);
const manualUpdateRequired = ref(false);
const reboot = ref(false);
async function onSubmit() {
@@ -31,6 +32,7 @@ async function onSubmit() {
if (certificateRenewalFailed.value) config.push('certificateRenewalFailed');
if (diskSpace.value) config.push('diskSpace');
if (cloudronUpdateFailed.value) config.push('cloudronUpdateFailed');
if (manualUpdateRequired.value) config.push('manualUpdateRequired');
if (reboot.value) config.push('reboot');
const [error] = await profileModel.setNotificationConfig(config);
@@ -55,6 +57,7 @@ async function open() {
certificateRenewalFailed.value = config.indexOf('certificateRenewalFailed') !== -1;
diskSpace.value = config.indexOf('diskSpace') !== -1;
cloudronUpdateFailed.value = config.indexOf('cloudronUpdateFailed') !== -1;
manualUpdateRequired.value = config.indexOf('manualUpdateRequired') !== -1;
reboot.value = config.indexOf('reboot') !== -1;
dialogItem.value.open();
@@ -121,6 +124,11 @@ defineExpose({
<Switch v-model="cloudronUpdateFailed" :disabled="busy"/>
</SettingsItem>
<SettingsItem>
<div>{{ $t('notifications.settings.manualUpdateRequired') }}</div>
<Switch v-model="manualUpdateRequired" :disabled="busy"/>
</SettingsItem>
<SettingsItem>
<div>{{ $t('notifications.settings.rebootRequired') }}</div>
<Switch v-model="reboot" :disabled="busy"/>

View File

@@ -105,6 +105,7 @@ onUnmounted(() => {
margin-bottom: 15px;
padding: 10px 15px;
padding-bottom: 25px;
overflow: hidden;
}
.section-header-title-badge {

View File

@@ -47,7 +47,7 @@ function validateForm() {
}
async function onSubmit() {
if (!form.value.reportValidity()) return;
if (!form.value.reportValidity() || !isFormValid.value) return;
busy.value = true;
formError.value = {};

View File

@@ -204,6 +204,7 @@ function onEdit(backup) {
editLabel.value = backup.label || '';
editError.value = '';
editDialog.value.open();
setTimeout(() => document.getElementById('labelInput').focus(), 500);
}
async function onEditSubmit() {

View File

@@ -1,6 +1,6 @@
<script setup>
const props = defineProps([ 'app' ]);
const props = defineProps([ 'app', 'refreshApp' ]);
import { ref, onMounted } from 'vue';
import { Button, Radiobutton, InputGroup, FormGroup, TextInput, SingleSelect } from '@cloudron/pankow';
@@ -55,6 +55,7 @@ async function onSendmailSubmit() {
return console.error(error);
}
await props.refreshApp();
sendmailBusy.value = false;
}
@@ -78,6 +79,7 @@ async function onRecvmailSubmit() {
return console.error(error);
}
await props.refreshApp();
recvmailBusy.value = false;
}

View File

@@ -8,7 +8,7 @@ import PortBindings from '../PortBindings.vue';
import AppsModel from '../../models/AppsModel.js';
import DomainsModel from '../../models/DomainsModel.js';
const props = defineProps([ 'app' ]);
const props = defineProps([ 'app', 'refreshApp' ]);
const appsModel = AppsModel.create();
const domainsModel = DomainsModel.create();
@@ -18,7 +18,7 @@ const busy = ref(false);
const errorMessage = ref('');
const errorObject = ref({});
const overwriteDns = ref(false);
const needsOverwriteDns = ref(false);
const needsOverwriteDns = ref([]);
const domain = ref('');
const subdomain = ref('');
const secondaryDomains = ref({});
@@ -56,6 +56,12 @@ function onAddRedirect() {
const form = useTemplateRef('form');
const isFormValid = ref(false);
function resetDnsOverwrite() {
needsOverwriteDns.value = [];
overwriteDns.value = false;
errorMessage.value = '';
}
function checkValidity() {
isFormValid.value = form.value ? form.value.checkValidity() : false;
@@ -87,7 +93,7 @@ async function onSubmit() {
busy.value = true;
errorMessage.value = '';
errorObject.value = {};
needsOverwriteDns.value = false;
needsOverwriteDns.value = [];
const checkForDomains = [{
domain: domain.value,
@@ -98,6 +104,7 @@ async function onSubmit() {
for (const d of aliases.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
for (const d of redirects.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
const conflicting = [];
for (const d of checkForDomains) {
const [error, result] = await domainsModel.checkRecords(d.domain, d.subdomain);
if (error) {
@@ -106,16 +113,19 @@ async function onSubmit() {
return console.error(error);
}
if (result.needsOverwrite && !overwriteDns.value) {
busy.value = false;
needsOverwriteDns.value = true;
return;
}
if (result.needsOverwrite) conflicting.push((d.subdomain ? d.subdomain + '.' : '') + d.domain);
}
if (conflicting.length > 0 && !overwriteDns.value) {
busy.value = false;
needsOverwriteDns.value = conflicting;
errorMessage.value = `DNS records of ${conflicting.join(', ')} already exist`;
return;
}
// only use enabled ports
const ports = {};
const portsCombined = Object.assign(tcpPorts.value || {}, udpPorts.value || {});
const portsCombined = Object.assign({}, tcpPorts.value || {}, udpPorts.value || {});
for (const env in portsCombined) {
if (portsCombined[env].enabled) {
ports[env] = portsCombined[env].value;
@@ -139,6 +149,7 @@ async function onSubmit() {
return console.error(error);
}
await props.refreshApp();
busy.value = false;
}
@@ -206,8 +217,8 @@ onMounted(async () => {
<div style="display: flex; gap: 10px;">
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" v-model="subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="domain" option-key="domain" option-label="label" :search-threshold="10" required/>
<TextInput style="flex-grow: 1" v-model="subdomain" @input="resetDnsOverwrite()" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10" required/>
</InputGroup>
<div class="warning-label" v-if="isNoopOrManual(domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((subdomain ? subdomain + '.' : '') + domain) })"></div>
<!-- Button just to offset the same margin on the right to align location input when alias or redirects are visible -->
@@ -219,8 +230,8 @@ onMounted(async () => {
<label :for="'secondaryDomainInput' + item.containerPort">{{ item.title }}</label>
<small>{{ item.description }}</small>
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" :id="'secondaryDomainInput' + item.containerPort" v-model="item.subdomain" :placeholder="$t('appstore.installDialog.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" :search-threshold="10" required/>
<TextInput style="flex-grow: 1" :id="'secondaryDomainInput' + item.containerPort" v-model="item.subdomain" @input="resetDnsOverwrite()" :placeholder="$t('appstore.installDialog.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10" required/>
</InputGroup>
<div class="warning-label" v-if="isNoopOrManual(item.domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((item.subdomain ? item.subdomain + '.' : '') + item.domain) })"></div>
</FormGroup>
@@ -233,8 +244,8 @@ onMounted(async () => {
<div v-for="(item, index) in aliases" :key="item" style="margin-bottom: 10px">
<div style="display: flex; gap: 10px;">
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" v-model="item.subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" :search-threshold="10"/>
<TextInput style="flex-grow: 1" v-model="item.subdomain" @input="resetDnsOverwrite()" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10"/>
</InputGroup>
<Button danger tool :disabled="busy" icon="fa-solid fa-trash" @click="onRemoveAlias(index)"/>
</div>
@@ -252,8 +263,8 @@ onMounted(async () => {
<div v-for="(item, index) in redirects" :key="item" style="margin-bottom: 10px;">
<div style="display: flex; gap: 10px;">
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" v-model="item.subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" :search-threshold="10"/>
<TextInput style="flex-grow: 1" v-model="item.subdomain" @input="resetDnsOverwrite()" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10"/>
</InputGroup>
<Button danger tool :disabled="busy" icon="fa-solid fa-trash" @click="onRemoveRedirect(index)"/>
</div>
@@ -271,13 +282,11 @@ onMounted(async () => {
<br/>
<div class="error-label" v-if="errorMessage">{{ errorMessage }}</div>
<br v-if="errorMessage"/>
<div class="text-danger" v-if="errorMessage">{{ errorMessage }}</div>
<Checkbox v-if="needsOverwriteDns.length" v-model="overwriteDns" :label="$t('app.location.overwriteDns', { domains: needsOverwriteDns.join(', ') })"/>
<br v-if="needsOverwriteDns.length"/>
<Checkbox v-if="needsOverwriteDns" v-model="overwriteDns" :label="$t('app.location.dnsoverwrite')"/>
<br v-if="needsOverwriteDns"/>
<Button @click="onSubmit()" :loading="busy" :disabled="busy || (app.error && app.error.installationState !== ISTATES.PENDING_LOCATION_CHANGE) || app.taskId || !isFormValid">{{ $t('app.location.saveAction') }}</Button>
<Button @click="onSubmit()" :loading="busy" :disabled="busy || (app.error && app.error.installationState !== ISTATES.PENDING_LOCATION_CHANGE) || app.taskId || !isFormValid || (needsOverwriteDns.length > 0 && !overwriteDns)">{{ $t('app.location.saveAction') }}</Button>
</div>
</template>

View File

@@ -6,7 +6,7 @@ import { taskNameFromInstallationState } from '../../utils.js';
import { ISTATES } from '../../constants.js';
import AppsModel from '../../models/AppsModel.js';
const props = defineProps([ 'app' ]);
const props = defineProps([ 'app', 'refreshApp' ]);
const appsModel = AppsModel.create();
const busyRepair = ref(false);
@@ -28,8 +28,8 @@ async function onToggleDebugMode() {
return console.error(error);
}
// let the task start
setTimeout(() => { debugModeBusy.value = false; }, 4000);
await props.refreshApp();
debugModeBusy.value = false;
}
async function onRepair() {
@@ -42,7 +42,8 @@ async function onRepair() {
return;
}
setTimeout(() => { busyRepair.value = false; }, 4000);
await props.refreshApp();
busyRepair.value = false;
}
async function onRestart() {

View File

@@ -10,7 +10,7 @@ import SystemModel from '../../models/SystemModel.js';
const appsModel = AppsModel.create();
const systemModel = SystemModel.create();
const props = defineProps([ 'app' ]);
const props = defineProps([ 'app', 'refreshApp' ]);
const memoryLimitBusy = ref(false);
const memoryLimit = ref(0);
@@ -33,8 +33,8 @@ async function onSubmitMemoryLimit() {
const [error] = await appsModel.configure(props.app.id, 'memory_limit', { memoryLimit: limit });
if (error) return console.error(error);
// give polling some time
setTimeout(() => memoryLimitBusy.value = false, 4000);
await props.refreshApp();
memoryLimitBusy.value = false;
}
async function onSubmitCpuQuota() {
@@ -44,9 +44,8 @@ async function onSubmitCpuQuota() {
if (error) return console.error(error);
currentCpuQuota.value = parseInt(cpuQuota.value);
// give polling some time
setTimeout(() => cpuQuotaBusy.value = false, 4000);
await props.refreshApp();
cpuQuotaBusy.value = false;
}
async function onSubmitDevices() {
@@ -70,11 +69,9 @@ async function onSubmitDevices() {
return;
}
// give polling some time
setTimeout(() => {
devicesBusy.value = false;
currentDevices.value = Object.keys(devs);
}, 4000);
currentDevices.value = Object.keys(devs);
await props.refreshApp();
devicesBusy.value = false;
}
const devicesChanged = computed(() => {
@@ -116,20 +113,20 @@ onMounted(async () => {
<FormGroup>
<label for="memoryLimitInput">{{ $t('app.resources.memory.title') }} <sup><a href="https://docs.cloudron.io/apps/#memory-limit" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ prettyBinarySize(memoryLimit, 'Default (256 MiB)') }}</b></label>
<div description>{{ $t('app.resources.memory.description') }}</div>
<input type="range" id="memoryLimitInput" v-model="memoryLimit" step="134217728" :min="memoryTicks[0]" :max="memoryTicks[memoryTicks.length-1]" list="memoryLimitTicks" />
<input type="range" id="memoryLimitInput" v-model="memoryLimit" step="134217728" :min="memoryTicks[0]" :max="memoryTicks[memoryTicks.length-1]" list="memoryLimitTicks" :disabled="memoryLimitBusy || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || !!app.taskId" />
<datalist id="memoryLimitTicks">
<option v-for="value of memoryTicks" :key="value" :value="value"></option>
</datalist>
</FormGroup>
<br/>
<Button @click="onSubmitMemoryLimit()" :loading="memoryLimitBusy" :disabled="memoryLimitBusy || (!app.error && memoryLimit === currentMemoryLimit) || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || app.taskId">{{ $t('app.resources.memory.resizeAction') }}</Button>
<Button @click="onSubmitMemoryLimit()" :loading="memoryLimitBusy" :disabled="memoryLimitBusy || (!app.error && memoryLimit == currentMemoryLimit) || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || !!app.taskId">{{ $t('app.resources.memory.resizeAction') }}</Button>
<hr style="margin-top: 20px"/>
<FormGroup>
<label for="cpuQuotaInput">{{ $t('app.resources.cpu.title') }} <sup><a href="https://docs.cloudron.io/apps/#cpu-limit" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ cpuQuota + ' %' }}</b></label>
<div description>{{ $t('app.resources.cpu.description') }}</div>
<input type="range" id="cpuQuotaInput" v-model="cpuQuota" step="1" min="1" max="100" list="cpuQuotaTicks" />
<input type="range" id="cpuQuotaInput" v-model="cpuQuota" step="1" min="1" max="100" list="cpuQuotaTicks" :disabled="cpuQuotaBusy || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || !!app.taskId" />
<datalist id="cpuQuotaTicks">
<option value="25"></option>
<option value="50"></option>
@@ -137,12 +134,12 @@ onMounted(async () => {
</datalist>
</FormGroup>
<br/>
<Button @click="onSubmitCpuQuota()" :loading="cpuQuotaBusy" :disabled="cpuQuotaBusy || (!app.error && cpuQuota === currentCpuQuota) || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || app.taskId">{{ $t('app.resources.cpu.setAction') }}</Button>
<Button @click="onSubmitCpuQuota()" :loading="cpuQuotaBusy" :disabled="cpuQuotaBusy || (!app.error && cpuQuota == currentCpuQuota) || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || !!app.taskId">{{ $t('app.resources.cpu.setAction') }}</Button>
<hr style="margin-top: 20px"/>
<form @submit.prevent="onSubmitDevices()" autocomplete="off">
<fieldset :disabled="devicesBusy || (!app.error && !devicesChanged) || (app.error && app.error.installationState !== ISTATES.PENDING_RECREATE_CONTAINER) || app.taskId">
<fieldset :disabled="devicesBusy || (app.error && app.error.installationState !== ISTATES.PENDING_RECREATE_CONTAINER) || !!app.taskId">
<input style="display: none;" type="submit"/>
<FormGroup>
<label for="devicesInput">{{ $t('app.resources.devices.label') }} <sup><a href="https://docs.cloudron.io/apps/#devices" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>

View File

@@ -6,7 +6,7 @@ import { ISTATES } from '../../constants.js';
import SettingsItem from '../SettingsItem.vue';
import AppsModel from '../../models/AppsModel.js';
const { app } = defineProps([ 'app' ]);
const { app, refreshApp } = defineProps([ 'app', 'refreshApp' ]);
const appsModel = AppsModel.create();
@@ -24,6 +24,7 @@ async function onTurnChange(value) {
return console.error(error);
}
await refreshApp();
turnBusy.value = false;
}
@@ -41,6 +42,7 @@ async function onRedisChange(value) {
return console.error(error);
}
await refreshApp();
redisBusy.value = false;
}

View File

@@ -10,7 +10,7 @@ import { ISTATES } from '../../constants.js';
import AppsModel from '../../models/AppsModel.js';
import VolumesModel from '../../models/VolumesModel.js';
const props = defineProps([ 'app' ]);
const props = defineProps([ 'app', 'refreshApp' ]);
const appsModel = AppsModel.create();
const volumesModel = VolumesModel.create();
@@ -56,9 +56,8 @@ async function onSubmitMove() {
}
originalVolumeId.value = volumeId.value;
// give app refresh some time, ideally we wait for the task
setTimeout(() => moveBusy.value = false, 4000);
await props.refreshApp();
moveBusy.value = false;
}
function onMountAdd() {
@@ -90,10 +89,9 @@ async function onSubmitMounts() {
return console.error(error);
}
// make a copy, cannot clone due to Proxy objects
originalMounts.value = mounts.value.map(m => { return { volumeId: m.volumeId, readOnly: m.readOnly }; });
setTimeout(() => mountsBusy.value = false, 2000);
await props.refreshApp();
mountsBusy.value = false;
}
const mountsValid = computed(() => {

View File

@@ -26,11 +26,14 @@ async function onUninstall() {
confirmLabel: t('app.uninstallDialog.uninstallAction'),
rejectLabel: t('main.dialog.cancel'),
rejectStyle: 'secondary',
autoCloseOnConfirm: false,
});
if (!yes) return;
const [error] = await appsModel.uninstall(props.app.id);
inputDialog.value.close();
if (error) return console.error(error);
window.location.href = '/#/apps';
@@ -44,12 +47,15 @@ async function onArchive() {
message: t('app.archiveDialog.description', { app: (props.app.label || props.app.fqdn), date: prettyLongDate(latestBackup.value.creationTime) }),
confirmStyle: 'danger',
confirmLabel: t('app.archive.action'),
rejectLabel: t('main.dialog.cancel')
rejectLabel: t('main.dialog.cancel'),
autoCloseOnConfirm: false,
});
if (!yes) return;
const [error] = await appsModel.archive(props.app.id, latestBackup.value.id);
inputDialog.value.close();
if (error) return console.error(error);
window.location.href = '/#/apps';

View File

@@ -293,11 +293,9 @@ const STORAGE_PROVIDERS = [
{ name: 'Cloudflare R2', value: 'cloudflare-r2' },
{ name: 'Contabo Object Storage', value: 'contabo-objectstorage', regions: REGIONS_CONTABO },
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces', regions: REGIONS_DIGITALOCEAN },
{ name: 'External/Local Disk (EXT4 or XFS)', value: 'disk' },
{ name: 'EXT4 Disk', value: 'ext4' },
{ name: 'Exoscale SOS', value: 'exoscale-sos', regions: REGIONS_EXOSCALE },
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Filesystem (Mount point)', value: 'mountpoint' }, // legacy
{ name: 'Google Cloud Storage', value: 'gcs' },
{ name: 'Hetzner Object Storage', value: 'hetzner-objectstorage', regions: REGIONS_HETZNER },
{ name: 'IDrive e2', value: 'idrive-e2' },
@@ -314,6 +312,7 @@ const STORAGE_PROVIDERS = [
{ name: 'Vultr Object Storage', value: 'vultr-objectstorage', regions: REGIONS_VULTR },
{ name: 'Wasabi', value: 'wasabi', regions: REGIONS_WASABI },
{ name: 'XFS Disk', value: 'xfs' },
{ name: 'User-managed Mount Point', value: 'mountpoint' },
];
const BACKUP_FORMATS = [

View File

@@ -82,7 +82,8 @@ export function create(type, id, options = {}) {
}
const time = data.realtimeTimestamp ? moment(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss') : '';
const html = ansiToHtml(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message)));
const escaped = ansiToHtml(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message)));
const html = escaped.replace(/\n/g, '<br>');
eventSource._lastMessage = { time, html };
lineHandler(time, html);

View File

@@ -6,10 +6,10 @@ const mountTypes = [
{ name: 'CIFS', value: 'cifs' },
{ name: 'EXT4', value: 'ext4' },
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Filesystem (Mount point)', value: 'mountpoint' },
{ name: 'NFS', value: 'nfs' },
{ name: 'SSHFS', value: 'sshfs' },
{ name: 'XFS', value: 'xfs' },
{ name: 'User-managed Mount Point', value: 'mountpoint' },
];
function filterConfigForMountType(mountType, config) {

View File

@@ -0,0 +1,16 @@
import { createApp } from 'vue';
import '@fontsource/inter';
import i18n from './i18n.js';
import OidcDeviceConfirmView from './views/OidcDeviceConfirmView.vue';
import './style.css';
(async function init() {
const app = createApp(OidcDeviceConfirmView);
app.use(await i18n());
app.mount('#app');
})();

View File

@@ -0,0 +1,16 @@
import { createApp } from 'vue';
import '@fontsource/inter';
import i18n from './i18n.js';
import OidcDeviceInputView from './views/OidcDeviceInputView.vue';
import './style.css';
(async function init() {
const app = createApp(OidcDeviceInputView);
app.use(await i18n());
app.mount('#app');
})();

View File

@@ -0,0 +1,16 @@
import { createApp } from 'vue';
import '@fontsource/inter';
import i18n from './i18n.js';
import OidcDeviceSuccessView from './views/OidcDeviceSuccessView.vue';
import './style.css';
(async function init() {
const app = createApp(OidcDeviceSuccessView);
app.use(await i18n());
app.mount('#app');
})();

View File

@@ -43,7 +43,7 @@ function download(filename, text) {
}
function mountlike(provider) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
}
function s3like(provider) {

View File

@@ -48,6 +48,8 @@ const sftpInfoDialog = useTemplateRef('sftpInfoDialog');
let refreshTimer = null;
async function refresh() {
clearTimeout(refreshTimer);
const [error, result] = await appsModel.get(id.value);
if (error) {
if (error.status === 403) return window.location.hash = '/';
@@ -340,19 +342,19 @@ onBeforeUnmount(() => {
<Transition name="slide-fade" mode="out-in">
<Info v-if="currentView === 'info'" :app="app"/>
<Display v-else-if="currentView === 'display'" :app="app"/>
<Location v-else-if="currentView === 'location'" :app="app"/>
<Location v-else-if="currentView === 'location'" :app="app" :refresh-app="refresh"/>
<Proxy v-else-if="currentView === 'proxy'" :app="app"/>
<Access v-else-if="currentView === 'access'" :app="app"/>
<Resources v-else-if="currentView === 'resources'" :app="app"/>
<Services v-else-if="currentView === 'services'" :app="app"/>
<Storage v-else-if="currentView === 'storage'" :app="app"/>
<Resources v-else-if="currentView === 'resources'" :app="app" :refresh-app="refresh"/>
<Services v-else-if="currentView === 'services'" :app="app" :refresh-app="refresh"/>
<Storage v-else-if="currentView === 'storage'" :app="app" :refresh-app="refresh"/>
<Graphs v-else-if="currentView === 'graphs'" :app="app"/>
<Security v-else-if="currentView === 'security'" :app="app"/>
<Email v-else-if="currentView === 'email'" :app="app"/>
<Email v-else-if="currentView === 'email'" :app="app" :refresh-app="refresh"/>
<Cron v-else-if="currentView === 'cron'" :app="app"/>
<Updates v-else-if="currentView === 'updates'" :app="app" :refresh-app="refresh"/>
<Backups v-else-if="currentView === 'backups'" :app="app"/>
<Repair v-else-if="currentView === 'repair'" :app="app"/>
<Repair v-else-if="currentView === 'repair'" :app="app" :refresh-app="refresh"/>
<Eventlog v-else-if="currentView === 'eventlog'" :app="app"/>
<Uninstall v-else-if="currentView === 'uninstall'" :app="app"/>
</Transition>

View File

@@ -171,7 +171,8 @@ function createAppLinkActionMenu(app) {
const filteredApps = computed(() => {
return apps.value.filter(a => {
if (a.type === APP_TYPES.LINK) {
return a.upstreamUri.includes(filter.value);
return a.upstreamUri.includes(filter.value)
|| (a.label ? a.label.toLowerCase().indexOf(filter.value.toLocaleLowerCase()) !== -1 : false);
} else { // app or proxy
return a.fqdn.includes(filter.value)
|| a.secondaryDomains.some(sd => sd.fqdn.includes(filter.value))

View File

@@ -100,7 +100,10 @@ async function onRemoveSite(site) {
if (!yes) return;
const [error] = await backupSitesModels.del(site.id);
if (error) console.error(error);
if (error) {
window.pankow.notify({ text: error.body?.message || 'Failed to delete backup site', type: 'danger' });
return console.error(error);
}
await refresh();

View File

@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, useTemplateRef, computed, inject } from 'vue';
import { Button, TableView, TextInput, InputDialog, ProgressBar } from '@cloudron/pankow';
import { Button, TableView, TextInput, InputDialog } from '@cloudron/pankow';
import Certificates from '../components/Certificates.vue';
import ActionBar from '../components/ActionBar.vue';
import SyncDns from '../components/SyncDns.vue';
@@ -150,8 +150,6 @@ onMounted(async () => {
<br/>
<ProgressBar v-if="busy" mode="indeterminate" :show-label="false" :slim="true" :show-track="false"/>
<TableView :model="filteredDomains" :columns="columns" :busy="busy" style="max-height: 450px;" :placeholder="$t(search ? 'domains.noMatchesPlaceholder' : 'domains.emptyPlaceholder')">
<template #provider="{ item:domain }">
{{ DomainsModel.prettyProviderName(domain.provider) }}

View File

@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, useTemplateRef, inject, computed } from 'vue';
import { Button, TableView, InputDialog, TextInput, ProgressBar } from '@cloudron/pankow';
import { Button, TableView, InputDialog, TextInput } from '@cloudron/pankow';
import ActionBar from '../components/ActionBar.vue';
import Section from '../components/Section.vue';
import GroupDialog from '../components/GroupDialog.vue';
@@ -163,8 +163,6 @@ onMounted(async () => {
<Button @click="onEditOrAddGroup()">{{ $t('main.action.add') }}</Button>
</template>
<ProgressBar v-if="busy" mode="indeterminate" :show-label="false" :slim="true" :show-track="false"/>
<TableView :columns="groupsColumns" :model="filteredGroups" :busy="busy" :fixed-layout="true" :placeholder="$t(searchFilter ? 'users.groups.noMatchesPlaceholder' : 'users.groups.emptyPlaceholder')">
<template #name="{ item:group }">
{{ group.name }} &nbsp; <i v-if="group.source" class="far fa-address-book" v-tooltip="$t('users.groups.externalLdapTooltip')"></i>

View File

@@ -163,13 +163,17 @@ onMounted(async () => {
<style scoped>
.section-body {
overflow: auto;
}
.notification-list {
display: flex;
gap: 10px;
flex-direction: column;
padding-bottom: 10px;
overflow-y: auto;
flex: 1;
overflow: auto;
height: 100%;
}
.notification-item {

View File

@@ -0,0 +1,61 @@
<script setup>
// keep this to load pankow Button css
import { Button } from '@cloudron/pankow';
import PublicPageLayout from '../components/PublicPageLayout.vue';
// coming from oidc_device_confirm.html server-side rendered
const clientName = window.cloudron.clientName;
const userCode = window.cloudron.userCode;
const form = window.cloudron.form;
</script>
<template>
<PublicPageLayout>
<div>
<h2>Authorize {{ clientName }}</h2>
<p>Verify the code below</p>
<div class="user-code">{{ userCode }}</div>
<p class="code-hint">If you did not initiate this action or the code does not match, please close this window or cancel.</p>
<!-- injected form for submission from oidcserver.js -->
<div v-html="form"></div>
<button class="pankow-button" type="submit" form="op.deviceConfirmForm">Continue</button>
<button type="submit" form="op.deviceConfirmForm" value="yes" name="abort" class="cancel-button">Cancel</button>
</div>
</PublicPageLayout>
</template>
<style scoped>
p {
padding: 10px 0;
}
.user-code {
font-size: 26px;
font-family: monospace;
}
.code-hint {
font-size: 12px;
}
.cancel-button {
margin-left: 15px;
background: none;
border: none;
padding: 0;
cursor: pointer;
font-size: 12px;
color: var(--pankow-color-dark);
}
.cancel-button:hover {
color: var(--pankow-color-primary-hover);
}
</style>

View File

@@ -0,0 +1,26 @@
<script setup>
import PublicPageLayout from '../components/PublicPageLayout.vue';
// coming from oidc_device_input.html server-side rendered
const message = window.cloudron.message;
const form = window.cloudron.form;
</script>
<template>
<PublicPageLayout>
<div>
<h2>Authorization canceled</h2>
<!-- in theory one can enter the code manually there, but we don't support this right now
<div v-html="message"></div>
<div v-html="form"></div>
<button type="submit" form="op.deviceInputForm">Continue</button>
-->
</div>
</PublicPageLayout>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,17 @@
<script setup>
import PublicPageLayout from '../components/PublicPageLayout.vue';
// coming from oidc_device_success.html server-side rendered (optional)
const cloudronName = window.cloudron.name;
</script>
<template>
<PublicPageLayout :cloudron-name="cloudronName">
<div style="max-width: 300px;">
<h2>Success</h2>
<p>Your device has been authorized. You can close this window.</p>
</div>
</PublicPageLayout>
</template>

View File

@@ -35,7 +35,7 @@ async function onPasswordReset() {
if (res.status === 409) {
error.value.generic = res.body.message;
} else if (res.status === 202) {
mode.value = MODE.NEW_PASSWORD_DONE;
mode.value = MODE.RESET_PASSWORD_DONE;
}
} catch (error) {
error.value.generic = error;
@@ -46,6 +46,8 @@ async function onPasswordReset() {
}
async function onNewPassword() {
if (newPassword.value !== newPasswordRepeat.value) return;
busy.value = true;
error.value = {};

View File

@@ -179,7 +179,7 @@ async function onSubmit() {
config.mountOptions.privateKey = providerConfig.value.mountOptionPrivateKey;
config.preserveAttributes = true;
}
} else if (provider.value === 'ext4' || provider.value === 'xfs' || provider.value === 'disk') {
} else if (provider.value === 'ext4' || provider.value === 'xfs') {
config.mountOptions.diskPath = providerConfig.value.mountOptionDiskPath;
config.preserveAttributes = true;
} else if (provider.value === 'mountpoint') {

View File

@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, computed, useTemplateRef, inject } from 'vue';
import { Button, TextInput, SingleSelect, TableView, InputDialog, ProgressBar } from '@cloudron/pankow';
import { Button, TextInput, SingleSelect, TableView, InputDialog } from '@cloudron/pankow';
import { ROLES } from '../constants.js';
import Section from '../components/Section.vue';
import ActionBar from '../components/ActionBar.vue';
@@ -293,8 +293,6 @@ onMounted(async () => {
<Button @click="onEditOrAddUser()">{{ $t('main.action.add') }}</Button>
</template>
<ProgressBar v-if="busy" mode="indeterminate" :show-label="false" :slim="true" :show-track="false"/>
<TableView :columns="usersColumns" :model="filteredUsers" :busy="busy" :fixed-layout="true" :placeholder="$t(search ? 'users.users.noMatchesPlaceholder' : 'users.users.emptyPlaceholder')">
<template #avatar="{ item:user }">
<img v-if="user.hasAvatar" :src="user.avatarUrl" @error="$event.target.src = '/img/avatar-default-symbolic.svg'" style="width: 30px; height: 30px; border-radius: 5px"/>

View File

@@ -267,6 +267,7 @@ onMounted(async () =>{
<FormGroup v-if="volumeDialogData.mountType === 'filesystem' || volumeDialogData.mountType === 'mountpoint'">
<label for="volumeHostPath">{{ $t('volumes.localDirectory') }}</label>
<TextInput id="volumeHostPath" v-model="volumeDialogData.hostPath" :placeholder="volumeDialogData.mountType === 'filesystem' ? '/srv/shared' : '/mnt/data'" required/>
<small class="warning-label" v-if="volumeDialogData.mountType === 'mountpoint'" v-html="$t('volumes.mountPointDescription', { docsLink: 'https://docs.cloudron.io/volumes/#user-managed-mount-point' })"></small>
</FormGroup>
<FormGroup v-if="volumeDialogData.mountType === 'ext4' || volumeDialogData.mountType === 'xfs'">
@@ -327,7 +328,7 @@ onMounted(async () =>{
<TableView :columns="columns" :model="volumes" :busy="busy" :fixed-layout="true" :placeholder="$t('volumes.emptyPlaceholder')">
<template #target="{ item:volume }">
<span v-if="volume.mountType === 'mountpoint' || volume.mountType === 'filesystem'">{{ volume.hostPath }}</span>
<span v-else-if="volume.mountType === 'ext4' || volume.mountType === 'xfs' || volume.mountType === 'disk'">{{ volume.mountOptions.diskPath }}</span>
<span v-else-if="volume.mountType === 'ext4' || volume.mountType === 'xfs'">{{ volume.mountOptions.diskPath }}</span>
<span v-else-if="volume.mountType === 'sshfs'">{{ volume.mountOptions.host + '/' + volume.mountOptions.remoteDir }}</span>
<!-- cifs/nfs -->
<span v-else>{{ volume.mountOptions.host + volume.mountOptions.remoteDir }}</span>

View File

@@ -1,6 +1,6 @@
'use strict';
var safe = require('safetydance');
var safe = require('@cloudron/safetydance').default;
exports.up = function(db, callback) {
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');

View File

@@ -1,7 +1,7 @@
'use strict';
var async = require('async'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings

View File

@@ -1,7 +1,7 @@
'use strict';
var async = require('async'),
safe = require('safetydance'),
safe = require('@cloudron/safetydance').default,
tld = require('tldjs');
exports.up = function(db, callback) {

View File

@@ -5,7 +5,7 @@ var async = require('async'),
fs = require('node:fs'),
os = require('node:os'),
path = require('node:path'),
safe = require('safetydance'),
safe = require('@cloudron/safetydance').default,
tldjs = require('tldjs');
exports.up = function(db, callback) {

View File

@@ -1,7 +1,7 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE domains ADD COLUMN wellKnownJson TEXT', function (error) {

View File

@@ -2,7 +2,7 @@
const async = require('async'),
fs = require('node:fs'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
const BOX_DATA_DIR = '/home/yellowtent/boxdata';
const PLATFORM_DATA_DIR = '/home/yellowtent/platformdata';

View File

@@ -2,7 +2,7 @@
const async = require('async'),
fs = require('node:fs'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
const BOX_DATA_DIR = '/home/yellowtent/boxdata';
const PLATFORM_DATA_DIR = '/home/yellowtent/platformdata';

View File

@@ -1,7 +1,7 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
const CERTS_DIR = '/home/yellowtent/boxdata/certs',
PLATFORM_CERTS_DIR = '/home/yellowtent/platformdata/nginx/cert';

View File

@@ -2,7 +2,7 @@
const async = require('async'),
fs = require('node:fs'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
const CERTS_DIR = '/home/yellowtent/boxdata/certs';

View File

@@ -4,7 +4,7 @@ const async = require('async'),
child_process = require('node:child_process'),
fs = require('node:fs'),
path = require('node:path'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
const OLD_CERTS_DIR = '/home/yellowtent/boxdata/certs';
const NEW_CERTS_DIR = '/home/yellowtent/platformdata/nginx/cert';

View File

@@ -1,7 +1,7 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
exports.up = function(db, callback) {
db.all('SELECT * FROM volumes', function (error, volumes) {

View File

@@ -1,7 +1,7 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
exports.up = function(db, callback) {
db.all('SELECT * from domains', [], function (error, results) {

View File

@@ -2,7 +2,7 @@
const async = require('async'),
openssl = require('../src/openssl.js').default,
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
const NGINX_CERT_DIR = '/home/yellowtent/platformdata/nginx/cert';

View File

@@ -3,7 +3,7 @@
const async = require('async'),
fs = require('node:fs'),
path = require('node:path'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
const MAIL_DATA_DIR = '/home/yellowtent/boxdata/mail';
const DKIM_DIR = `${MAIL_DATA_DIR}/dkim`;

View File

@@ -1,6 +1,6 @@
'use strict';
const safe = require('safetydance');
const safe = require('@cloudron/safetydance').default;
const PROXY_AUTH_TOKEN_SECRET_FILE = '/home/yellowtent/platformdata/proxy-auth-token-secret';

View File

@@ -2,7 +2,7 @@
const async = require('async'),
mail = require('../src/mail.js').default,
safe = require('safetydance'),
safe = require('@cloudron/safetydance').default,
util = require('node:util');
// it seems some mail domains do not have dkimKey in the database for some reason because of some previous bad migration

View File

@@ -2,7 +2,7 @@
const crypto = require('node:crypto'),
path = require('node:path'),
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
function getMountPoint(dataDir) {
const output = safe.child_process.execSync(`df --output=target "${dataDir}" | tail -1`, { encoding: 'utf8' });

View File

@@ -1,6 +1,6 @@
'use strict';
const safe = require('safetydance');
const safe = require('@cloudron/safetydance').default;
exports.up = async function (db) {
const mailDomains = await db.runSql('SELECT * FROM mail', []);

View File

@@ -4,7 +4,7 @@ const crypto = require('node:crypto'),
fs = require('node:fs'),
path = require('node:path'),
paths = require('../src/paths.js').default,
safe = require('safetydance');
safe = require('@cloudron/safetydance').default;
exports.up = async function(db) {
const backups = await db.runSql('SELECT format, COUNT(*) AS count FROM backups GROUP BY format WITH ROLLUP', []); // https://dev.mysql.com/doc/refman/8.4/en/group-by-modifiers.html

View File

@@ -2,10 +2,11 @@
exports.up = async function(db) {
const results = await db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern']);
if (results.length === 0) return; // use defaults from box code
let policy, schedule;
if (results.length === 0 || results[0].value === 'never') {
if (results[0].value === 'never') {
policy = 'never';
schedule = '00 00 1,3,5,23 * * *';
} else {

View File

@@ -0,0 +1,7 @@
exports.up = async function (db) {
await db.runSql('ALTER TABLE apps ADD COLUMN buildConfigJson TEXT');
};
exports.down = async function (db) {
await db.runSql('ALTER TABLE apps DROP COLUMN buildConfigJson');
};

View File

@@ -0,0 +1,46 @@
'use strict';
const child_process = require('node:child_process');
function patchMountFile(mountPath, fstype) {
const mountFilename = child_process.execSync(`systemd-escape -p --suffix=mount "${mountPath}"`, { encoding: 'utf8' }).trim();
const mountFile = `/etc/systemd/system/${mountFilename}`;
try {
child_process.execSync(`sed -i 's/^Type=auto$/Type=${fstype}/' "${mountFile}"`);
child_process.execSync('systemctl daemon-reload');
console.log(`Patched ${mountFile}: Type=auto -> Type=${fstype}`);
} catch (e) {
console.log(`Warning: failed to patch ${mountFile}: ${e.message}`);
}
}
exports.up = async function (db) {
const results = await db.runSql('SELECT id, configJson FROM backupSites WHERE provider = ?', ['disk']);
for (const row of results) {
const config = JSON.parse(row.configJson);
let fstype = 'ext4';
try {
const diskPath = config.mountOptions.diskPath;
const output = child_process.execSync(`lsblk --paths --json --list --fs ${diskPath}`, { encoding: 'utf8' });
const info = JSON.parse(output);
if (info.blockdevices[0].fstype === 'xfs') fstype = 'xfs';
} catch (e) {
console.log(`Could not detect filesystem type for backup site ${row.id}, defaulting to ext4: ${e.message}`);
}
config._provider = fstype;
console.log(`Migrating backup site ${row.id} from disk to ${fstype}`);
await db.runSql('UPDATE backupSites SET provider = ?, configJson = ? WHERE id = ?', [fstype, JSON.stringify(config), row.id]);
if (config._managedMountPath) {
patchMountFile(config._managedMountPath, fstype);
}
}
};
exports.down = function (db, callback) {
callback();
};

View File

@@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS users(
active BOOLEAN DEFAULT 1,
avatar MEDIUMBLOB,
backgroundImage MEDIUMBLOB,
language VARCHAR(8) NOT NULL DEFAULT "",
loginLocationsJson MEDIUMTEXT, // { locations: [{ ip, userAgent, city, country, ts }] }
notificationConfigJson TEXT,
@@ -81,14 +82,16 @@ CREATE TABLE IF NOT EXISTS apps(
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
memoryLimit BIGINT DEFAULT 0,
cpuQuota INTEGER DEFAULT 100,
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
proxyAuth BOOLEAN DEFAULT 0, // whether proxy auth is enabled
devicesJson TEXT,
debugModeJson TEXT, // options for development mode
reverseProxyConfigJson TEXT, // { robotsTxt, csp, hstsPreload }
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
enableMailbox BOOLEAN DEFAULT 1, // whether sendmail addon is enabled
enableTurn BOOLEAN DEFAULT 1,
enableRedis BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app
mailboxDomain VARCHAR(128), // mailbox domain of this app
mailboxDisplayName VARCHAR(128), // mailbox display name
@@ -102,6 +105,8 @@ CREATE TABLE IF NOT EXISTS apps(
storageVolumePrefix VARCHAR(128),
taskId INTEGER, // current task
errorJson TEXT,
operatorsJson TEXT,
updateInfoJson TEXT,
servicesConfigJson TEXT, // app services configuration
containerIp VARCHAR(16) UNIQUE, // this is not-null because of ip allocation fails, user can 'repair'
packageIcon MEDIUMBLOB,
@@ -109,6 +114,7 @@ CREATE TABLE IF NOT EXISTS apps(
crontab TEXT,
upstreamUri VARCHAR(256) DEFAULT "",
checklistJson TEXT, // checklist for admins
buildConfigJson TEXT, // { buildArgs, dockerfileName } for source installs
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(inboxDomain) REFERENCES domains(domain),

11
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@cloudron/connect-lastmile": "^3.0.0",
"@cloudron/manifest-format": "^6.1.0",
"@cloudron/pipework": "^2.1.2",
"@cloudron/safetydance": "^3.0.1",
"@cloudron/superagent": "^2.1.1",
"@google-cloud/dns": "^5.3.1",
"@google-cloud/storage": "^7.19.0",
@@ -46,7 +47,6 @@
"oidc-provider": "^9.6.1",
"ovh": "^2.0.3",
"qrcode": "^1.5.4",
"safetydance": "^2.5.1",
"semver": "^7.7.4",
"speakeasy": "^2.0.0",
"tar-stream": "^3.1.8",
@@ -7774,15 +7774,6 @@
"version": "2.1.2",
"license": "MIT"
},
"node_modules/safetydance": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-2.5.1.tgz",
"integrity": "sha512-loeEErOTR8rhC2ICec1C1dRyfOtjFomS2A1JG8rl5qNQWVHJMJNKdL/kinaSq58yu1mOak0UW+pKY1cb/t4BCg==",
"engines": [
"node >= 4.0.0"
],
"license": "MIT"
},
"node_modules/sax": {
"version": "1.2.1",
"license": "ISC"

View File

@@ -51,7 +51,7 @@
"oidc-provider": "^9.6.1",
"ovh": "^2.0.3",
"qrcode": "^1.5.4",
"safetydance": "^2.5.1",
"@cloudron/safetydance": "^3.0.1",
"semver": "^7.7.4",
"speakeasy": "^2.0.0",
"tar-stream": "^3.1.8",

View File

@@ -885,8 +885,17 @@ function check_services() {
local service_port=("3000" "3000" "3000" "3000" "2003" "3000")
for service in "${!services[@]}"; do
if [[ $(docker inspect ${services[$service]} --format={{.State.Status}}) != "running" ]]; then
fail "Service '${services[$service]}' container is not running!"
local service_name="${services[$service]}"
local service_state
if ! service_state="$(docker inspect "${service_name}" --format={{.State.Status}} 2>/dev/null)"; then
service_state="missing"
fi
if [[ "${service_state}" != "running" ]]; then
if [[ "${service_state}" == "exited" ]] && [[ "${service_name}" == "mysql" || "${service_name}" == "postgresql" || "${service_name}" == "mongodb" ]]; then
warn "Service '${service_name}' is not running (may be lazy-stopped)"
else
fail "Service '${service_name}' container is not running (state: ${service_state})!"
fi
continue
fi

View File

@@ -56,7 +56,10 @@ echo "==> Building dashboard assets"
rm -rf "${bundle_dir}/dashboard/node_modules"
echo "==> Installing toplevel node modules"
(cd "${bundle_dir}" && npm ci --omit=dev --omit=optional --tldjs-update-rules)
(cd "${bundle_dir}" && npm ci --omit=dev --omit=optional)
echo "==> Update tldjs rules"
(cd "${bundle_dir}" && node node_modules/tldjs/bin/update.js)
echo "==> Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)

View File

@@ -8,7 +8,7 @@ import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { program } from 'commander';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import { Client as SshClient } from 'ssh2';
import util from 'node:util';

View File

@@ -7,7 +7,7 @@ import net from 'net';
import os from 'os';
import path from 'path';
import { program } from 'commander';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import semver from 'semver';
import superagent from '@cloudron/superagent';
import Table from 'easy-table';

View File

@@ -113,7 +113,7 @@ $ip6tables -t filter -A CLOUDRON -p udp --sport 547 --dport 546 -j ACCEPT
ipxtables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
# for ldap,dockerproxy server (ipv4 only) to accept connections from apps. for connecting to addons and mail container ports, docker already has rules
$iptables -t filter -A CLOUDRON -p tcp -s 172.18.0.0/16 -d 172.18.0.1 -m multiport --dports 3002,3003 -j ACCEPT
$iptables -t filter -A CLOUDRON -p tcp -s 172.18.0.0/16 -d 172.18.0.1 -m multiport --dports 3002,3003,3006 -j ACCEPT
$iptables -t filter -A CLOUDRON -p udp -s 172.18.0.0/16 --dport 53 -j ACCEPT # dns responses from docker (127.0.0.11)
ipxtables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)

View File

@@ -7,8 +7,8 @@ import dns from './dns.js';
import openssl from './openssl.js';
import path from 'node:path';
import paths from './paths.js';
import promiseRetry from './promise-retry.js';
import safe from 'safetydance';
import retry from './retry.js';
import safe from '@cloudron/safetydance';
import superagent from '@cloudron/superagent';
import users from './users.js';
@@ -244,7 +244,7 @@ Acme2.prototype.waitForOrder = async function (orderUrl) {
log(`waitForOrder: ${orderUrl}`);
return await promiseRetry({ times: 15, interval: 20000, log }, async () => {
return await retry({ times: 15, interval: 20000, log }, async () => {
log('waitForOrder: getting status');
const result = await this.postAsGet(orderUrl);
@@ -297,7 +297,7 @@ Acme2.prototype.waitForChallenge = async function (challenge) {
log(`waitingForChallenge: ${JSON.stringify(challenge)}`);
await promiseRetry({ times: 15, interval: 20000, log }, async () => {
await retry({ times: 15, interval: 20000, log }, async () => {
log('waitingForChallenge: getting status');
const result = await this.postAsGet(challenge.url);
@@ -335,7 +335,7 @@ Acme2.prototype.signCertificate = async function (finalizationUrl, csrPem) {
Acme2.prototype.downloadCertificate = async function (certUrl) {
assert.strictEqual(typeof certUrl, 'string');
return await promiseRetry({ times: 5, interval: 20000, log }, async () => {
return await retry({ times: 5, interval: 20000, log }, async () => {
log(`downloadCertificate: downloading certificate of ${this.cn}`);
const result = await this.postAsGet(certUrl);
@@ -471,7 +471,7 @@ Acme2.prototype.acmeFlow = async function () {
const challenge = await this.prepareChallenge(cn, authorization);
await this.notifyChallengeReady(challenge);
await this.waitForChallenge(challenge);
await safe(this.cleanupChallenge(cn, challenge), { log });
await safe(this.cleanupChallenge(cn, challenge), { debug: log });
}
const csr = await openssl.createCsr(this.key, this.cn, this.altNames);
@@ -485,7 +485,7 @@ Acme2.prototype.acmeFlow = async function () {
};
Acme2.prototype.loadDirectory = async function () {
await promiseRetry({ times: 3, interval: 20000, log }, async () => {
await retry({ times: 3, interval: 20000, log }, async () => {
const response = await superagent.get(this.caDirectory).timeout(30000).ok(() => true);
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching directory : ${response.status}`);
@@ -522,7 +522,7 @@ async function getCertificate(fqdn, domainObject, key) {
const owner = await users.getOwner();
const email = owner?.email || 'webmaster@cloudron.io'; // can error if not activated yet
return await promiseRetry({ times: 3, interval: 0, log }, async function () {
return await retry({ times: 3, interval: 0, log }, async function () {
log(`getCertificate: for fqdn ${fqdn} and domain ${domainObject.domain}`);
const acme = new Acme2(fqdn, domainObject, email, key, { /* profile: 'shortlived' */ });

View File

@@ -6,7 +6,7 @@ import constants from './constants.js';
import logger from './logger.js';
import docker from './docker.js';
import eventlog from './eventlog.js';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import superagent from '@cloudron/superagent';
const { log } = logger('apphealthmonitor');

View File

@@ -5,7 +5,7 @@ import crypto from 'node:crypto';
import database from './database.js';
import logger from './logger.js';
import jsdom from 'jsdom';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import superagent from '@cloudron/superagent';
const { log } = logger('applinks');

View File

@@ -2,13 +2,22 @@ import assert from 'node:assert';
import BoxError from './boxerror.js';
import crypto from 'node:crypto';
import database from './database.js';
import hat from './hat.js';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import _ from './underscore.js';
const APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime', 'expiresAt' ].join(',');
function generatePassword() {
const blocks = [];
for (let b = 0; b < 4; b++) {
let block = '';
for (let i = 0; i < 4; i++) block += String.fromCharCode(97 + crypto.randomInt(26));
blocks.push(block);
}
return blocks.join('-');
}
function validateAppPasswordName(name) {
assert.strictEqual(typeof name, 'string');
@@ -41,7 +50,7 @@ async function add(userId, identifier, name, expiresAt) {
if (identifier.length < 1) throw new BoxError(BoxError.BAD_FIELD, 'identifier must be atleast 1 char');
const password = hat(16 * 4);
const password = generatePassword();
const hashedPassword = crypto.createHash('sha256').update(password).digest('base64');
const appPassword = {

View File

@@ -29,7 +29,7 @@ import path from 'node:path';
import paths from './paths.js';
import { PassThrough } from 'node:stream';
import reverseProxy from './reverseproxy.js';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import semver from 'semver';
import services from './services.js';
import shellModule from './shell.js';
@@ -78,7 +78,7 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.versionsUrl',
'apps.sso', 'apps.devicesJson', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab',
'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate', 'apps.upstreamUri', 'apps.checklistJson', 'apps.updateInfoJson',
'apps.enableMailbox', 'apps.mailboxDisplayName', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableInbox', 'apps.inboxName', 'apps.inboxDomain',
'apps.enableTurn', 'apps.enableRedis', 'apps.storageVolumeId', 'apps.storageVolumePrefix', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.packageIcon IS NOT NULL) AS hasPackageIcon' ].join(',');
'apps.enableTurn', 'apps.enableRedis', 'apps.storageVolumeId', 'apps.storageVolumePrefix', 'apps.ts', 'apps.healthTime', 'apps.buildConfigJson', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.packageIcon IS NOT NULL) AS hasPackageIcon' ].join(',');
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId', 'count' ].join(',');
const LOCATION_FIELDS = [ 'appId', 'subdomain', 'domain', 'type', 'certificateJson' ];
@@ -412,6 +412,10 @@ function postProcess(result) {
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
assert(result.buildConfigJson === null || typeof result.buildConfigJson === 'string');
result.buildConfig = safe.JSON.parse(result.buildConfigJson) || { buildArgs: [], dockerfileName: null };
delete result.buildConfigJson;
const subdomains = JSON.parse(result.subdomains),
parsedDomains = JSON.parse(result.domains),
subdomainTypes = JSON.parse(result.subdomainTypes),
@@ -524,7 +528,7 @@ function pickFields(app, level) {
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'notes', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'devices', 'env', 'enableAutomaticUpdate',
'storageVolumeId', 'storageVolumePrefix', 'mounts', 'enableTurn', 'enableRedis', 'checklist',
'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'updateInfo']);
'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'updateInfo', 'buildConfig']);
}
// remove private certificate key
@@ -606,7 +610,8 @@ async function add(id, appStoreId, versionsUrl, manifest, subdomain, domain, por
notes = data.notes || null,
crontab = data.crontab || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true;
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
buildConfigJson = data.buildConfig ? JSON.stringify(data.buildConfig) : null;
// when redis is optional, do not enable it by default. it's mostly used for caching in those setups
const enableRedis = 'enableRedis' in data ? data.enableRedis : !manifest.addons?.redis?.optional;
@@ -618,12 +623,12 @@ async function add(id, appStoreId, versionsUrl, manifest, subdomain, domain, por
queries.push({
query: 'INSERT INTO apps (id, appStoreId, versionsUrl, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon, '
+ 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab, enableBackup, enableAutomaticUpdate) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
+ 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab, enableBackup, enableAutomaticUpdate, buildConfigJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, versionsUrl, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota,
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon,
enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab,
enableBackup, enableAutomaticUpdate
enableBackup, enableAutomaticUpdate, buildConfigJson
]
});
@@ -744,24 +749,27 @@ async function updateWithConstraints(id, app, constraints) {
if ('subdomain' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'DELETE FROM locations WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.subdomain, Location.TYPE_PRIMARY ]});
}
if ('secondaryDomains' in app) {
app.secondaryDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_SECONDARY, d.environmentVariable ]});
});
}
if ('secondaryDomains' in app) {
queries.push({ query: 'DELETE FROM locations WHERE appId = ? AND type = ?', args: [ id, Location.TYPE_SECONDARY ]});
app.secondaryDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_SECONDARY, d.environmentVariable ]});
});
}
if ('redirectDomains' in app) {
app.redirectDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_REDIRECT ]});
});
}
if ('redirectDomains' in app) {
queries.push({ query: 'DELETE FROM locations WHERE appId = ? AND type = ?', args: [ id, Location.TYPE_REDIRECT ]});
app.redirectDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_REDIRECT ]});
});
}
if ('aliasDomains' in app) {
app.aliasDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_ALIAS ]});
});
}
if ('aliasDomains' in app) {
queries.push({ query: 'DELETE FROM locations WHERE appId = ? AND type = ?', args: [ id, Location.TYPE_ALIAS ]});
app.aliasDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_ALIAS ]});
});
}
if ('mounts' in app) {
@@ -774,7 +782,7 @@ async function updateWithConstraints(id, app, constraints) {
const fields = [ ], values = [ ];
for (const p in app) {
if (p === 'manifest' || p === 'tags' || p === 'checklist' || p === 'accessRestriction' || p === 'devices' || p === 'debugMode' || p === 'error'
|| p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators' || p === 'updateInfo') {
|| p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators' || p === 'updateInfo' || p === 'buildConfig') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'subdomain' && p !== 'domain' && p !== 'secondaryDomains' && p !== 'redirectDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') {
@@ -1369,7 +1377,8 @@ async function appendLogLine(app, line) {
const logFilePath = path.join(paths.LOG_DIR, app.id, 'app.log');
const isoDate = new Date(new Date().toUTCString()).toISOString();
if (!safe.fs.appendFileSync(logFilePath, `${isoDate} ${line}\n`)) console.error(`Could not append log line for app ${app.id} at ${logFilePath}: ${safe.error.message}`);
const escaped = line.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
if (!safe.fs.appendFileSync(logFilePath, `${isoDate} ${escaped}\n`)) console.error(`Could not append log line for app ${app.id} at ${logFilePath}: ${safe.error.message}`);
}
async function checkManifest(manifest) {
@@ -1605,7 +1614,7 @@ async function loadConfig(app) {
const appConfig = safe.JSON.parse(safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json')));
let data = {};
if (appConfig) {
data = _.pick(appConfig, ['memoryLimit', 'cpuQuota', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate']);
data = _.pick(appConfig, ['memoryLimit', 'cpuQuota', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate', 'buildConfig']);
}
const icon = safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png'));
@@ -1921,9 +1930,14 @@ async function install(data, auditSource) {
if (data.sourceArchiveFilePath) await fileUtils.renameFile(data.sourceArchiveFilePath, `${paths.SOURCE_ARCHIVES_DIR}/${appId}.tar.gz`);
const buildConfig = {
buildArgs: data.buildConfig?.buildArgs || [],
dockerfileName: data.buildConfig?.dockerfileName || null
};
const task = {
args: { restoreConfig: null, skipDnsSetup, overwriteDns },
values: { },
values: { buildConfig },
requiredState: app.installationState
};
@@ -2340,6 +2354,10 @@ async function updateApp(app, data, auditSource) {
const updateConfig = { skipBackup, manifest }; // this will clear appStoreId/versionsUrl when updating from a repo and set it if passed in for update route
if ('appStoreId' in data) updateConfig.appStoreId = data.appStoreId;
if ('versionsUrl' in data) updateConfig.versionsUrl = data.versionsUrl;
updateConfig.buildConfig = {
buildArgs: data.buildConfig?.buildArgs || [],
dockerfileName: data.buildConfig?.dockerfileName || null
};
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
@@ -2388,6 +2406,8 @@ async function updateApp(app, data, auditSource) {
if (data.sourceArchiveFilePath) await fileUtils.renameFile(data.sourceArchiveFilePath, `${paths.SOURCE_ARCHIVES_DIR}/${appId}.tar.gz`);
values.buildConfig = updateConfig.buildConfig;
const task = {
args: { updateConfig },
values
@@ -2483,6 +2503,19 @@ async function restore(app, backupId, auditSource) {
values.inboxName = values.inboxDomain = null;
}
// prune secondaryDomains whose httpPorts no longer exist in the restored manifest
const newHttpPorts = manifest.httpPorts || {};
values.secondaryDomains = app.secondaryDomains.filter(sd => sd.environmentVariable in newHttpPorts);
// prune portBindings whose tcpPorts/udpPorts no longer exist in the restored manifest
const newTcpPorts = manifest.tcpPorts || {};
const newUdpPorts = manifest.udpPorts || {};
const portBindings = {};
for (const portName in app.portBindings) {
if (portName in newTcpPorts || portName in newUdpPorts) portBindings[portName] = app.portBindings[portName];
}
values.portBindings = portBindings;
const restoreConfig = { backupId: restoreBackup.id };
const task = {
@@ -2624,7 +2657,7 @@ async function clone(app, data, user, auditSource) {
const dolly = _.pick(cloneBackup.appConfig || app, ['memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'devices',
'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'debugMode',
'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso',
'notes', 'checklist']);
'notes', 'checklist', 'buildConfig']);
if (!manifest.addons?.recvmail) dolly.inboxDomain = null; // needed because we are cloning _current_ app settings with old manifest
@@ -2700,7 +2733,7 @@ async function unarchive(archiveEntry, data, auditSource) {
const dolly = _.pick(archiveBackup.appConfig || {}, ['memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig',
'tags', 'label', 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'devices',
'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso',
'notes', 'checklist']);
'notes', 'checklist', 'buildConfig']);
// intentionally not filled up: redirectDomain, aliasDomains, mailboxDomain
const obj = Object.assign(dolly, {

View File

@@ -14,8 +14,8 @@ import mail from './mail.js';
import manifestFormat from '@cloudron/manifest-format';
import oidcClients from './oidcclients.js';
import paths from './paths.js';
import promiseRetry from './promise-retry.js';
import safe from 'safetydance';
import retry from './retry.js';
import safe from '@cloudron/safetydance';
import semver from 'semver';
import settings from './settings.js';
import superagent from '@cloudron/superagent';
@@ -81,11 +81,22 @@ async function getState() {
return acc;
}, {});
const systemLanguage = await settings.get(settings.LANGUAGE_KEY) || 'en';
const userLanguageCounts = allUsers.reduce((acc, u) => {
const lang = u.language || 'default';
acc[lang] = (acc[lang] || 0) + 1;
return acc;
}, {});
const state = {
provider: system.getProvider(),
users: { count: allUsers.length, roleCounts },
groupCount: (await groups.list()).length,
domains: (await domains.list()).map(d => d.provider),
language: {
system: systemLanguage,
users: userLanguageCounts
},
mail: {
incomingCount: mailDomains.filter(md => md.enabled).length,
catchAllCount: mailDomains.filter(md => md.catchAll.length).length,
@@ -388,7 +399,7 @@ async function getApp(appId) {
async function downloadIcon(appStoreId, version) {
const iconUrl = `${await getApiServerOrigin()}/api/v1/apps/${appStoreId}/versions/${version}/icon`;
return await promiseRetry({ times: 10, interval: 5000, log }, async function () {
return await retry({ times: 10, interval: 5000, log }, async function () {
const [networkError, response] = await safe(superagent.get(iconUrl)
.timeout(60 * 1000)
.ok(() => true));

View File

@@ -21,9 +21,9 @@ import manifestFormat from '@cloudron/manifest-format';
import os from 'node:os';
import path from 'node:path';
import paths from './paths.js';
import promiseRetry from './promise-retry.js';
import retry from './retry.js';
import reverseProxy from './reverseproxy.js';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import services from './services.js';
import shellModule from './shell.js';
import _ from './underscore.js';
@@ -63,7 +63,7 @@ async function allocateContainerIp(app) {
if (app.manifest.id === constants.PROXY_APP_APPSTORE_ID) return;
await promiseRetry({ times: 10, interval: 0, log }, async function () {
await retry({ times: 10, interval: 0, log }, async function () {
const iprange = iputils.intFromIp(constants.APPS_IPv4_END) - iputils.intFromIp(constants.APPS_IPv4_START);
const rnd = Math.floor(Math.random() * iprange);
const containerIp = iputils.ipFromInt(iputils.intFromIp(constants.APPS_IPv4_START) + rnd);
@@ -198,8 +198,6 @@ async function downloadImage(manifest) {
async function buildLocalImage(app) {
assert.strictEqual(typeof app, 'object');
// TODO some precondition checks like downloadImage maybe
const sourceFilePath = path.join(paths.APPS_DATA_DIR, app.id, 'source.tar.gz');
// if we have a newly uploaded source archive, use that
@@ -220,7 +218,7 @@ async function buildLocalImage(app) {
}
}
await docker.buildImage(app.manifest.dockerImage, sourceFilePath);
await docker.buildImage(app.manifest.dockerImage, sourceFilePath, app.buildConfig);
}
async function updateChecklist(app, newChecks, acknowledged = false) {
@@ -331,7 +329,8 @@ async function uninstallCommand(app, args, progressCallback) {
await deleteContainers(app, {});
await progressCallback({ percent: 30, message: 'Teardown addons' });
await services.teardownAddons(app, app.manifest.addons);
// if install/clone/restore/import failed early (e.g. invalid image), services may not have started
await services.teardownAddons(app, app.manifest.addons, { ignoreError: true });
await services.teardownPersistentDirs(app);
await progressCallback({ percent: 40, message: 'Cleanup file manager' });
@@ -408,7 +407,7 @@ async function installCommand(app, args, progressCallback) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
if (oldManifest) {
const addonsToRemove = _.omit(oldManifest.addons, Object.keys(app.manifest.addons));
await services.teardownAddons(app, addonsToRemove);
await services.teardownAddons(app, addonsToRemove, {});
}
if (!restoreConfig || restoreConfig.remotePath || restoreConfig.backupId) { // install/import/restore but not in-place import should delete data dir
@@ -602,7 +601,7 @@ async function changeServicesCommand(app, args, progressCallback) {
if (app.manifest.addons?.redis && !app.enableRedis) unusedAddons.redis = app.manifest.addons.redis;
await progressCallback({ percent: 20, message: 'Removing unused addons' });
await services.teardownAddons(app, unusedAddons);
await services.teardownAddons(app, unusedAddons, {});
await progressCallback({ percent: 40, message: 'Setting up addons' });
await services.setupAddons(app, app.manifest.addons);
@@ -658,11 +657,16 @@ async function configureCommand(app, args, progressCallback) {
await progressCallback({ percent: 40, message: 'Downloading image' });
await downloadImage(app.manifest);
await progressCallback({ percent: 45, message: 'Ensuring app data directory' });
if (app.manifest.dockerImage.indexOf('local/') === 0) {
await progressCallback({ percent: 45, message: 'Building image' });
await buildLocalImage(app);
}
await progressCallback({ percent: 50, message: 'Ensuring app data directory' });
await createAppDir(app);
// re-setup addons since they rely on the app's fqdn (e.g oauth)
await progressCallback({ percent: 50, message: 'Setting up addons' });
await progressCallback({ percent: 55, message: 'Setting up addons' });
await services.setupAddons(app, app.manifest.addons);
await progressCallback({ percent: 60, message: 'Creating container' });
@@ -688,6 +692,7 @@ async function updateCommand(app, args, progressCallback) {
// FIXME: this does not handle option changes (like multipleDatabases)
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
const httpPortChanged = app.manifest.httpPort !== updateConfig.manifest.httpPort;
const httpPortsChanged = !_.isEqual(app.manifest.httpPorts, updateConfig.manifest.httpPorts);
const proxyAuthChanged = !_.isEqual(app.manifest.addons?.proxyAuth, updateConfig.manifest.addons?.proxyAuth);
// this protects against the theoretical possibility of an app being marked for update from
@@ -727,7 +732,7 @@ async function updateCommand(app, args, progressCallback) {
if (app.manifest.dockerImage !== updateConfig.manifest.dockerImage) await docker.deleteImage(app.manifest.dockerImage);
// only delete unused addons after backup
await services.teardownAddons(app, unusedAddons);
await services.teardownAddons(app, unusedAddons, {});
if (Object.keys(unusedAddons).includes('localstorage')) await updateApp(app, { storageVolumeId: null, storageVolumePrefix: null }); // lose reference
// teardown persistent dirs removed in the new manifest
@@ -758,6 +763,7 @@ async function updateCommand(app, args, progressCallback) {
if (environmentVariable in newHttpPorts) continue; // domain still in use
secondaryDomains.splice(i, 1); // remove domain
}
const removedSecondaryDomains = app.secondaryDomains.filter(sd => !secondaryDomains.some(nsd => nsd.subdomain === sd.subdomain && nsd.domain === sd.domain));
const values = {
manifest: updateConfig.manifest,
@@ -774,6 +780,8 @@ async function updateCommand(app, args, progressCallback) {
await updateApp(app, values); // switch over to the new config
if (removedSecondaryDomains.length) await dns.unregisterLocations(removedSecondaryDomains, progressCallback);
await progressCallback({ percent: 45, message: 'Downloading icon' });
await downloadIcon(app);
@@ -793,7 +801,7 @@ async function updateCommand(app, args, progressCallback) {
await startApp(app);
await progressCallback({ percent: 90, message: 'Configuring reverse proxy' });
if (proxyAuthChanged || httpPortChanged) {
if (proxyAuthChanged || httpPortChanged || httpPortsChanged || removedSecondaryDomains.length) {
await reverseProxy.configureApp(app, AuditSource.APPTASK);
}
@@ -867,7 +875,7 @@ async function run(appId, args, progressCallback) {
log('run: update aborted because backup failed');
await safe(updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, { log }));
} else {
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }), { log });
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }), { debug: log });
}
throw error;

View File

@@ -5,7 +5,7 @@ import fs from 'node:fs';
import locks from './locks.js';
import path from 'node:path';
import paths from './paths.js';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
import scheduler from './scheduler.js';
import tasks from './tasks.js';

View File

@@ -3,7 +3,7 @@ import BoxError from './boxerror.js';
import crypto from 'node:crypto';
import database from './database.js';
import eventlog from './eventlog.js';
import safe from 'safetydance';
import safe from '@cloudron/safetydance';
const ARCHIVE_FIELDS = [ 'archives.id', 'backupId', 'archives.creationTime', 'backups.remotePath', 'backups.siteId', 'backups.manifestJson', 'backups.appConfigJson', '(archives.icon IS NOT NULL) AS hasIcon', '(archives.packageIcon IS NOT NULL) AS hasPackageIcon' ];

Some files were not shown because too many files have changed in this diff Show More