Compare commits

..

1 Commits

Author SHA1 Message Date
Johannes Zellner cfe7bb53e6 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 21:38:37 +02:00
51 changed files with 397 additions and 1177 deletions
-5
View File
@@ -3219,8 +3219,3 @@
* 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
-106
View File
@@ -1,106 +0,0 @@
# Plan: Adding PowerDNS as a DNS Provider to Cloudron
This document outlines the detailed steps required to add support for PowerDNS via the PowerDNS Authoritative Server REST API to Cloudron. It covers frontend UI updates, backend DNS provider implementation, unit testing, and live testing instructions.
## 1. Frontend Modifications
The Cloudron dashboard is a Vue application that needs to know about the new provider and how to prompt the user for credentials.
### `dashboard/src/models/DomainsModel.js`
* **Add Provider:** Append `{ name: 'PowerDNS', value: 'powerdns' }` to the `providers` array.
* **Define Required Properties:** In the `getProviderConfigProps(provider)` switch statement, add a case for PowerDNS to define the fields it needs:
```javascript
case 'powerdns':
props = ['apiUrl', 'apiKey'];
break;
```
### `dashboard/src/components/DomainProviderForm.vue`
* **Reset Logic:** In the `dnsConfig` reset block (around line 57), add `dnsConfig.value.apiUrl = '';` to ensure the form clears properly.
* **UI Template:** Add the HTML form groups for PowerDNS within the template section:
```html
<!-- powerdns -->
<FormGroup v-if="provider === 'powerdns'">
<label for="powerdnsApiUrlInput">{{ $t('domains.domainDialog.powerdnsApiUrl') }}</label>
<TextInput id="powerdnsApiUrlInput" type="url" v-model="dnsConfig.apiUrl" placeholder="https://ns1.example.com:8081" required />
</FormGroup>
<FormGroup v-if="provider === 'powerdns'">
<label for="powerdnsApiKeyInput">{{ $t('domains.domainDialog.powerdnsApiKey') }}</label>
<MaskedInput id="powerdnsApiKeyInput" v-model="dnsConfig.apiKey" required />
</FormGroup>
```
### `dashboard/public/translation/en.json`
* **Add Translations:** Add the English translation strings for the new fields under the `domains.domainDialog` object:
```json
"powerdnsApiUrl": "PowerDNS API URL (e.g., https://ns1.example.com:8081)",
"powerdnsApiKey": "API Key",
```
## 2. Backend Modifications
The backend needs a new driver file that implements the standard Cloudron DNS provider interface.
### `src/dns.js`
* **Import the Driver:** Add `import dnsPowerdns from './dns/powerdns.js';` at the top with the other imports.
* **Register the Provider:** Add `powerdns: dnsPowerdns` to the `DNS_PROVIDERS` mapping object.
### `src/dns/powerdns.js` (New File)
Create a new file that implements the standard DNS interface (`src/dns/interface.js`).
* **Required Methods:** Export `removePrivateFields`, `injectPrivateFields`, `upsert`, `get`, `del`, `wait`, and `verifyDomainConfig`.
* **API Interactions:**
* Construct the base URL: `const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');`
* Ensure queries and zone names have a trailing dot, as PowerDNS strictly requires absolute FQDNs (e.g., `example.com.`).
* Set the `X-API-Key` header using `domainConfig.apiKey` for all requests.
* **Operations:**
* `get`: Use `GET ${baseUrl}/api/v1/servers/localhost/zones/${zoneName}.` and extract records from the `rrsets` array matching the exact `fqdn` and `type`.
* `upsert`: Use `PATCH ${baseUrl}/api/v1/servers/localhost/zones/${zoneName}.`. Send an `rrsets` payload with `changetype: 'REPLACE'`. Ensure `TXT` values are quoted.
* `del`: Use `PATCH` with `changetype: 'DELETE'`.
* **verifyDomainConfig:** Perform a test `upsert` and `del` of an `A` record on the subdomain `cloudrontestdns` to ensure the API is reachable and the key has edit permissions.
## 3. Automated Testing (Local)
The new provider needs to be verified against the existing test suite using mock HTTP requests.
### `src/test/dns-providers-test.js`
* Add a new `describe('powerdns', ...)` block.
* Setup mock domain configuration with a dummy `apiUrl` and `apiKey`.
* Use `nock` to intercept requests to the dummy `apiUrl`.
* Write tests for:
* `upsert` non-existing and existing records.
* `get` records.
* `del` records.
* Ensure the nock endpoints expect the exact JSON structure and `rrsets` payload required by PowerDNS.
### Running the Tests
Run the specific test suite for DNS providers locally:
```bash
./run-tests src/test/dns-providers-test.js
```
*(This uses the fast-path to run mocha directly, skipping the full Cloudron test environment setup).*
## 4. Live Testing (Remote Cloudron Server)
To test the integration end-to-end, deploy the changes to a temporary Cloudron instance using the provided hotfix script.
**Important Considerations for Hotfixing:**
The `scripts/hotfix` tool builds a release tarball by executing `git archive HEAD`. **You must commit your changes locally before running the hotfix**, otherwise the script will fail or push the old codebase.
1. **Commit Changes:**
```bash
git add .
git commit -m "Implement PowerDNS provider"
```
2. **Run Hotfix Script:**
Execute the hotfix script from the repository root, pointing it to your test server.
```bash
./scripts/hotfix --cloudron <IP_OR_DOMAIN_OF_TEST_SERVER> --ssh-user root --ssh-key ~/.ssh/id_rsa --release 1.0.0-test
```
3. **Verify Deployment:**
* The script will install dependencies, compile the Vue dashboard, bundle the backend code, push it to the server, and restart the `box` service.
* Log into the Cloudron dashboard via your browser.
* Navigate to Domains -> Add Domain, select "PowerDNS".
* Enter a test domain, a valid PowerDNS API URL, and an API Key.
* Verify that Cloudron successfully configures the domain and creates the necessary initial DNS records.
+2 -4
View File
@@ -38,10 +38,8 @@ async function setupNetworking() {
function exitSync(status) {
const ts = new Date().toISOString();
if (status.message) fs.write(logFd, `${ts} ${status.message}\n`, function () {});
if (status.error) {
const escapedStack = status.error.stack.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
fs.write(logFd, `${ts} ${escapedStack}\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 () {});
fs.fsyncSync(logFd);
fs.closeSync(logFd);
process.exit(status.code);
+201 -203
View File
@@ -5,7 +5,7 @@
"packages": {
"": {
"dependencies": {
"@cloudron/pankow": "^4.1.10",
"@cloudron/pankow": "^4.1.8",
"@fontsource/inter": "^5.2.8",
"@fortawesome/fontawesome-free": "^7.2.0",
"@simplewebauthn/browser": "^13.3.0",
@@ -17,15 +17,15 @@
"async": "^3.2.6",
"chart.js": "^4.5.1",
"chartjs-plugin-annotation": "^3.1.0",
"eslint": "^10.2.0",
"eslint": "^10.1.0",
"eslint-plugin-vue": "^10.8.0",
"marked": "^18.0.0",
"marked": "^17.0.5",
"moment": "^2.30.1",
"moment-timezone": "^0.6.1",
"vite": "^8.0.8",
"vite": "^8.0.3",
"vite-plugin-singlefile": "^2.3.2",
"vue": "^3.5.32",
"vue-i18n": "^11.3.2",
"vue": "^3.5.31",
"vue-i18n": "^11.3.0",
"vue-router": "^5.0.4"
}
},
@@ -92,9 +92,9 @@
}
},
"node_modules/@cloudron/pankow": {
"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==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@cloudron/pankow/-/pankow-4.1.8.tgz",
"integrity": "sha512-JahcrQPVJPGvw+c+6T1k/KVqm4oTHIux5kHvGbk9O+gECoCbkYEhdLDKN6oGWb7UlvtfRRNYPN/gxpcPeCJCfw==",
"license": "ISC",
"dependencies": {
"@fontsource/inter": "^5.2.8",
@@ -154,12 +154,12 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.23.5",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^3.0.5",
"@eslint/object-schema": "^3.0.3",
"debug": "^4.3.1",
"minimatch": "^10.2.4"
},
@@ -168,21 +168,21 @@
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
"integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==",
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
"integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.2.1"
"@eslint/core": "^1.1.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
"integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
@@ -192,21 +192,21 @@
}
},
"node_modules/@eslint/object-schema": {
"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==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
"integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
"integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
"integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.2.1",
"@eslint/core": "^1.1.1",
"levn": "^0.4.1"
},
"engines": {
@@ -280,14 +280,14 @@
}
},
"node_modules/@intlify/core-base": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.2.tgz",
"integrity": "sha512-cgsUaV/dyD6aS49UPgerIblrWeXAZHNaDWqm4LujOGC7IafSyhghGXEiSVvuDYaDPiQTP+tSFSTM1HIu7Yp1nA==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
"integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
"license": "MIT",
"dependencies": {
"@intlify/devtools-types": "11.3.2",
"@intlify/message-compiler": "11.3.2",
"@intlify/shared": "11.3.2"
"@intlify/devtools-types": "11.3.0",
"@intlify/message-compiler": "11.3.0",
"@intlify/shared": "11.3.0"
},
"engines": {
"node": ">= 16"
@@ -297,13 +297,13 @@
}
},
"node_modules/@intlify/devtools-types": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.2.tgz",
"integrity": "sha512-q96G2ZZw0FNoXzejbjIf9dbfgz1xyYBZu6ZT4b5TE/55j8d1O9X5jv0k+U+L3fVe7uebPcqRQFD0ffm30i5mJA==",
"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==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.3.2",
"@intlify/shared": "11.3.2"
"@intlify/core-base": "11.3.0",
"@intlify/shared": "11.3.0"
},
"engines": {
"node": ">= 16"
@@ -313,12 +313,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.2.tgz",
"integrity": "sha512-d/awyHUkNSaGPxBxT/qlUpfRizxHX9dt55CnW03xx5p1KmMyfYHKupCnvzINX+Na8JR8LAR7y32lPKjoeQGmzA==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
"integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.3.2",
"@intlify/shared": "11.3.0",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -329,9 +329,9 @@
}
},
"node_modules/@intlify/shared": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.2.tgz",
"integrity": "sha512-x66fjdH6i+lNYPae5URSQGTjBL68Av6hi09jvC5Ci96iTkwfqrPhCj46aylQZmgMaG89rOZCIKqS7ApC8ZDVjg==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
"integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -392,9 +392,9 @@
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"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==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -410,18 +410,18 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
"integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
"cpu": [
"arm64"
],
@@ -435,9 +435,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
"cpu": [
"arm64"
],
@@ -451,9 +451,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
"integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
"cpu": [
"x64"
],
@@ -467,9 +467,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
"integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
"cpu": [
"x64"
],
@@ -483,9 +483,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
"integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
"cpu": [
"arm"
],
@@ -499,9 +499,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
"cpu": [
"arm64"
],
@@ -515,9 +515,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
"integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
"cpu": [
"arm64"
],
@@ -531,9 +531,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
"cpu": [
"ppc64"
],
@@ -547,9 +547,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
"cpu": [
"s390x"
],
@@ -563,9 +563,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
"cpu": [
"x64"
],
@@ -579,9 +579,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
"integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
"cpu": [
"x64"
],
@@ -595,9 +595,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
"cpu": [
"arm64"
],
@@ -611,27 +611,25 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
"integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.9.2",
"@emnapi/runtime": "1.9.2",
"@napi-rs/wasm-runtime": "^1.1.3"
"@napi-rs/wasm-runtime": "^1.1.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
"integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
"cpu": [
"arm64"
],
@@ -645,9 +643,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
"integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
"cpu": [
"x64"
],
@@ -1086,39 +1084,39 @@
}
},
"node_modules/@vue/compiler-core": {
"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==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
"integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.2",
"@vue/shared": "3.5.32",
"@vue/shared": "3.5.31",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
"integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz",
"integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.32",
"@vue/shared": "3.5.32"
"@vue/compiler-core": "3.5.31",
"@vue/shared": "3.5.31"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
"integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz",
"integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==",
"license": "MIT",
"dependencies": {
"@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",
"@vue/compiler-core": "3.5.31",
"@vue/compiler-dom": "3.5.31",
"@vue/compiler-ssr": "3.5.31",
"@vue/shared": "3.5.31",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.8",
@@ -1126,13 +1124,13 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
"integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz",
"integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.32",
"@vue/shared": "3.5.32"
"@vue/compiler-dom": "3.5.31",
"@vue/shared": "3.5.31"
}
},
"node_modules/@vue/devtools-api": {
@@ -1160,53 +1158,53 @@
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
"integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz",
"integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.32"
"@vue/shared": "3.5.31"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
"integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz",
"integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.32",
"@vue/shared": "3.5.32"
"@vue/reactivity": "3.5.31",
"@vue/shared": "3.5.31"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
"integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz",
"integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.32",
"@vue/runtime-core": "3.5.32",
"@vue/shared": "3.5.32",
"@vue/reactivity": "3.5.31",
"@vue/runtime-core": "3.5.31",
"@vue/shared": "3.5.31",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
"integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz",
"integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.32",
"@vue/shared": "3.5.32"
"@vue/compiler-ssr": "3.5.31",
"@vue/shared": "3.5.31"
},
"peerDependencies": {
"vue": "3.5.32"
"vue": "3.5.31"
}
},
"node_modules/@vue/shared": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
"integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
"integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
"license": "MIT"
},
"node_modules/@xterm/addon-attach": {
@@ -1337,9 +1335,9 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -1512,18 +1510,18 @@
}
},
"node_modules/eslint": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz",
"integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz",
"integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.4",
"@eslint/config-helpers": "^0.5.4",
"@eslint/core": "^1.2.0",
"@eslint/plugin-kit": "^0.7.0",
"@eslint/config-array": "^0.23.3",
"@eslint/config-helpers": "^0.5.3",
"@eslint/core": "^1.1.1",
"@eslint/plugin-kit": "^0.6.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@@ -2264,9 +2262,9 @@
}
},
"node_modules/marked": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.0.tgz",
"integrity": "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA==",
"version": "17.0.5",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz",
"integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@@ -2301,12 +2299,12 @@
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -2647,13 +2645,13 @@
}
},
"node_modules/rolldown": {
"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==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
"integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
"@oxc-project/types": "=0.122.0",
"@rolldown/pluginutils": "1.0.0-rc.12"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -2662,27 +2660,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@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"
"@rolldown/binding-android-arm64": "1.0.0-rc.12",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
"@rolldown/binding-darwin-x64": "1.0.0-rc.12",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
"integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
"license": "MIT"
},
"node_modules/rollup": {
@@ -2883,16 +2881,16 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
"integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.15",
"rolldown": "1.0.0-rc.12",
"tinyglobby": "^0.2.15"
},
"bin": {
@@ -2910,7 +2908,7 @@
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"esbuild": "^0.27.0 || ^0.28.0",
"esbuild": "^0.27.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
@@ -2977,17 +2975,17 @@
}
},
"node_modules/vue": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
"integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@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"
"@vue/compiler-dom": "3.5.31",
"@vue/compiler-sfc": "3.5.31",
"@vue/runtime-dom": "3.5.31",
"@vue/server-renderer": "3.5.31",
"@vue/shared": "3.5.31"
},
"peerDependencies": {
"typescript": "*"
@@ -3023,14 +3021,14 @@
}
},
"node_modules/vue-i18n": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.2.tgz",
"integrity": "sha512-gmFrvM+iuf2AH4ygligw/pC7PRJ63AdRNE68E0GPlQ83Mzfyck6g6cRQC3KzkYXr+ZidR91wq+5YBmAMpkgE1A==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.3.2",
"@intlify/devtools-types": "11.3.2",
"@intlify/shared": "11.3.2",
"@intlify/core-base": "11.3.0",
"@intlify/devtools-types": "11.3.0",
"@intlify/shared": "11.3.0",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
+6 -6
View File
@@ -8,7 +8,7 @@
"type": "module",
"dependencies": {
"@simplewebauthn/browser": "^13.3.0",
"@cloudron/pankow": "^4.1.10",
"@cloudron/pankow": "^4.1.8",
"@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.2.0",
"eslint": "^10.1.0",
"eslint-plugin-vue": "^10.8.0",
"marked": "^18.0.0",
"marked": "^17.0.5",
"moment": "^2.30.1",
"moment-timezone": "^0.6.1",
"vite": "^8.0.8",
"vite": "^8.0.3",
"vite-plugin-singlefile": "^2.3.2",
"vue": "^3.5.32",
"vue-i18n": "^11.3.2",
"vue": "^3.5.31",
"vue-i18n": "^11.3.0",
"vue-router": "^5.0.4"
}
}
+1 -2
View File
@@ -972,8 +972,7 @@
"saveAction": "Uložit",
"aliases": "Aliasy",
"addAliasAction": "Přidat alias",
"noAliases": "Žádné aliasy pro domény",
"overwriteDns": "Přepsat existující DNS záznamy pro {domains}"
"noAliases": "Žádné aliasy pro domény"
},
"accessControl": {
"userManagement": {
+1 -4
View File
@@ -825,8 +825,6 @@
"domain": "Domain",
"provider": "DNS provider",
"route53AccessKeyId": "Access key ID",
"powerdnsApiUrl": "PowerDNS API URL (e.g., https://ns1.example.com:8081)",
"powerdnsApiKey": "API Key",
"route53SecretAccessKey": "Secret access key",
"gcdnsServiceAccountKey": "Service account key",
"digitalOceanToken": "DigitalOcean token",
@@ -1636,8 +1634,7 @@
"editVolumeDialog": {
"title": "Edit Volume"
},
"emptyPlaceholder": "No volumes",
"mountPointDescription": "The mount point has to be set up manually. See <a href=\"{{ docsLink }}\" target=\"_blank\">docs</a>."
"emptyPlaceholder": "No volumes"
},
"newLoginEmail": {
"subject": "[<%= cloudron %>] New login on your account",
+57 -440
View File
@@ -15,8 +15,7 @@
"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…",
@@ -27,26 +26,14 @@
"save": "Sauvegarder",
"no": "Non",
"yes": "Oui",
"delete": "Supprimer",
"edit": "Editer",
"done": "Terminer"
"delete": "Supprimer"
},
"username": "Nom d'utilisateur",
"actions": "Actions",
"displayName": "Nom affiché",
"action": {
"logs": "Journaux",
"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"
"reboot": "Redémarrer"
},
"rebootDialog": {
"rebootAction": "Redémarrer maintenant",
@@ -60,20 +47,9 @@
},
"statusEnabled": "Activé",
"navbar": {
"users": "Utilisateurs",
"groups": "Groupes"
"users": "Utilisateurs"
},
"loadingPlaceholder": "Chargement",
"table": {
"version": "Version",
"created": "Créé"
},
"sidebar": {
"collapseAction": "Réduire la barre latérale"
},
"platform": {
"startupFailed": "Échec du démarrage de la plateforme"
}
"loadingPlaceholder": "Chargement"
},
"users": {
"users": {
@@ -88,22 +64,17 @@
"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",
"noMatchesPlaceholder": "Aucun utilisateur correspondant",
"emptyPlaceholder": "Aucun utilisateur"
"mailmanagerTooltip": "Cet utilisateur peut gérer les utilisateurs et les boîtes mail"
},
"groups": {
"name": "Nom",
"users": "Utilisateurs",
"externalLdapTooltip": "Depuis un annuaire LDAP externe",
"emptyPlaceholder": "Aucun groupe",
"noMatchesPlaceholder": "Aucun groupe correspondant"
"externalLdapTooltip": "Depuis un annuaire LDAP externe"
},
"settings": {
"allowProfileEditCheckbox": "Autoriser les utilisateurs à modifier leur nom et leur adresse email",
"saveAction": "Enregistrer",
"require2FACheckbox": "Demander aux utilisateurs une authentification à deux facteurs (2FA)",
"title": "Paramètres"
"require2FACheckbox": "Demander aux utilisateurs une authentification à deux facteurs (2FA)"
},
"externalLdap": {
"configureAction": "Paramétrer",
@@ -157,8 +128,7 @@
"group": {
"users": "Utilisateurs",
"name": "Nom",
"addGroupAction": "Ajouter un groupe",
"allowedApps": "Applications autorisées"
"addGroupAction": "Ajouter un groupe"
},
"deleteGroupDialog": {
"title": "Supprimer le groupe {{ name }}",
@@ -221,14 +191,7 @@
"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",
"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"
"cloudflarePortWarning": "Le proxy Cloudflare doit être désactivé sur le domaine du tableau de bord pour accéder au service LDAP"
}
},
"profile": {
@@ -253,8 +216,7 @@
"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.",
"expires": "Date d'expiration"
"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."
},
"changeEmail": {
"title": "Modifier l'adresse email principale",
@@ -266,8 +228,7 @@
"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 :",
"expiresAt": "Date d'expiration"
"description": "Utilisez le mot de passe suivant pour vous authentifier auprès de l'application :"
},
"changeFallbackEmail": {
"title": "Modifier l'adresse email de récupération du mot de passe"
@@ -276,20 +237,14 @@
"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.",
"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."
"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."
},
"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",
"allowedIpRanges": "Plage(s) d'adresses IP autorisées"
"access": "Accès API"
},
"changePasswordAction": "Modifier le mot de passe",
"apiTokens": {
@@ -301,9 +256,7 @@
"lastUsed": "Dernière utilisation",
"scope": "Portée",
"readonly": "Lecture seule",
"readwrite": "Lecture et écriture",
"allowedIpRangesPlaceholder": "Adresses IP ou sous-réseaux séparés par des virgules",
"allowedIpRanges": "Adresses IP autorisées"
"readwrite": "Lecture et écriture"
},
"loginTokens": {
"logoutAll": "Déconnecter de tous",
@@ -312,32 +265,6 @@
},
"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": {
@@ -349,9 +276,7 @@
"days": "Jours",
"hours": "Heures",
"title": "Paramétrer la planification et la conservation des sauvegardes",
"retentionPolicy": "Politique de conservation",
"disable": "Désactiver les sauvegardes automatiques",
"enable": "Activer les sauvegardes automatiques"
"retentionPolicy": "Politique de conservation"
},
"schedule": {
"title": "Planification et conservation",
@@ -399,36 +324,13 @@
"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",
"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"
"encryptFilenames": "Chiffré les nom de fichiers"
},
"backupDetails": {
"title": "Informations sur la sauvegarde",
"id": "ID",
"date": "Date",
"version": "Version",
"size": "Taille",
"duration": "Durée de la sauvegarde",
"lastIntegrityCheck": "Dernier contrôle d'intégrité",
"integrityNever": "Jamais",
"integrityInProgress": "En cours"
"version": "Version"
},
"listing": {
"title": "Liste",
@@ -450,45 +352,12 @@
"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.",
"setAction": "Définir l'emplacement"
"description": "Cela déplacera le serveur IMAP et SMTP vers l'emplacement choisi."
},
"eventlog": {
"details": "Détails",
@@ -509,9 +378,7 @@
"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",
"savedInfo": "Enregistré",
"sentInfo": "Envoyé"
"quota": "Quota de boîte mail"
},
"title": "Journal des événements de la messagerie",
"mailFrom": "De",
@@ -533,8 +400,7 @@
"title": "Domaines",
"outbound": "Sortant uniquement",
"stats": "{{ mailboxCount }} adresse(s) de messagerie / utilisation : {{ usage }}",
"testEmailTooltip": "Envoyer un email test",
"inbound": "Entrant et sortant"
"testEmailTooltip": "Envoyer un email test"
},
"testMailDialog": {
"title": "Envoyer un email test pour {{ domain }}",
@@ -628,12 +494,7 @@
"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",
"account": "Compte",
"unlinkAction": "Dissocier le compte",
"unlinkDialog": {
"title": "Désassocier le compte Cloudron.io"
}
"emailNotVerified": "Adresse email pas encore confirmée"
},
"registryConfig": {
"provider": "Fournisseur du registre Docker",
@@ -662,24 +523,11 @@
"stopUpdateAction": "Interrompre la mise à jour",
"updateAvailableAction": "Mise à jour disponible",
"checkForUpdatesAction": "Rechercher les mises à jour disponibles",
"title": "Mises à jour",
"disabled": "Désactivé",
"onLatest": "dernier",
"config": "Mises à jour automatiques",
"appsOnly": "Applications uniquement",
"platformAndApps": "Plateforme et applications"
"title": "Mises à jour"
},
"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": {
@@ -690,28 +538,7 @@
},
"notifications": {
"dismissTooltip": "Supprimer",
"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"
"markAllAsRead": "Tout marquer comme lu"
},
"appstore": {
"category": {
@@ -741,14 +568,10 @@
"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",
"ephemeralPortWarning": "L'utilisation de ports éphémères peut entraîner des conflits imprévisibles."
"portReadOnly": "lecture seule"
},
"unstable": "Instable",
"searchPlaceholder": "Rechercher des solutions alternatives telles que GitHub, Dropbox, Slack, Trello…",
"action": {
"addCustomApp": "Ajouter une application personnalisée"
}
"searchPlaceholder": "Rechercher des solutions alternatives telles que GitHub, Dropbox, Slack, Trello…"
},
"app": {
"updatesTabTitle": "Mises à jour",
@@ -758,14 +581,7 @@
"lastUpdated": "Dernière mise à jour",
"packageVersion": "Version du package",
"appId": "ID 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."
"description": "Nom et version de l'application"
}
},
"backupsTabTitle": "Sauvegardes",
@@ -793,27 +609,10 @@
"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)",
"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"
}
"title": "Politique de sécurité du contenu (CSP)"
},
"robots": {
"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"
"title": "Robots.txt"
},
"hstsPreload": "Activer HSTS pour ce site et tous les sous-domaines"
},
@@ -843,27 +642,18 @@
"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",
"disableAction": "Désactiver le mode de récupération",
"enableAction": "Activer le mode de récupération"
"title": "Récupération après un crash"
},
"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": {
@@ -895,18 +685,13 @@
"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",
"cloneAction": "Cloner",
"cloneActionOverwrite": "Cloner et écraser le DNS"
"restoreAction": "Restaurer"
},
"importBackupDialog": {
"uploadAction": "Charger le fichier de configuration de la sauvegarde",
"title": "Importer la sauvegarde",
"importAction": "Importer",
"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."
"remotePath": "Chemin de la sauvegarde"
},
"repairTabTitle": "Réparation",
"uninstallDialog": {
@@ -916,10 +701,7 @@
},
"appInfo": {
"package": "Package",
"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"
"openAction": "Ouvrir {{ app }}"
},
"firstTimeSetupAction": "Initialisation",
"uninstall": {
@@ -946,8 +728,7 @@
"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",
"checkIntegrity": "Vérifier l'intégrité"
"downloadBackupTooltip": "Télécharger la sauvegarde"
}
},
"graphs": {
@@ -956,9 +737,7 @@
"7d": "7 jours",
"24h": "24 heures",
"12h": "12 heures",
"6h": "6 heures",
"live": "En direct",
"1h": "1 heure"
"6h": "6 heures"
},
"diskIOTotal": "total: lecture {{ read }} / écriture {{ write }}",
"networkIOTotal": "total: entrant {{ inbound }} / sortant {{ outbound }}"
@@ -973,10 +752,6 @@
"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": {
@@ -1046,42 +821,10 @@
},
"servicesTabTitle": "Services",
"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."
"title": "Configuration de TURN"
},
"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."
"title": "Configuration de Redis"
}
},
"logs": {
@@ -1093,8 +836,7 @@
"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",
"title": "Supprimer le volume"
"removeAction": "Supprimer"
},
"addVolumeDialog": {
"title": "Ajouter un volume",
@@ -1121,9 +863,7 @@
"description": "Le texte ci-dessous s'affichera dans tous les emails sortants de ce domaine.",
"plainTextFormat": "Format texte",
"htmlFormat": "Format HTML (optionnel)",
"title": "Signature",
"customSignatureSet": "Signature personnalisée configurée",
"noSignatureSet": "Aucune signature configurée"
"title": "Signature"
},
"incoming": {
"catchall": {
@@ -1136,9 +876,7 @@
"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",
"emptyPlaceholder": "Pas de listes de diffusion",
"noMatchesPlaceholder": "Aucune liste de diffusion correspondante"
"membersOnlyTooltip": "Utilisation de la liste limitée à ses membres"
},
"mailboxes": {
"usage": "Utilisation",
@@ -1146,9 +884,7 @@
"title": "Messageries",
"owner": "Propriétaire",
"name": "Nom",
"addAction": "Ajouter",
"emptyPlaceholder": "Pas de boîtes aux lettres",
"noMatchesPlaceholder": "Aucune boîte aux lettres correspondante"
"addAction": "Ajouter"
},
"sieveServerInfo": "ManageSieve",
"incomingServerInfo": "Réception (IMAP)",
@@ -1159,8 +895,7 @@
"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",
"description": "Recevoir les e-mails entrants pour ce domaine"
"incomingPasswordUsage": "Mot de passe du propriétaire de la boîte mail"
},
"addMailinglistDialog": {
"members": "Liste des membres",
@@ -1176,8 +911,7 @@
},
"addMailboxDialog": {
"title": "Ajouter une adresse de messagerie",
"name": "Nom",
"incomingDisabledWarning": "La réception des e-mails pour ce domaine n'est pas activée"
"name": "Nom"
},
"editMailboxDialog": {
"title": "Paramétrer l'adresse de messagerie {{ name }}@{{ domain }}",
@@ -1203,9 +937,7 @@
},
"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.",
"outboundSmtp": "SMTP sortant",
"rblCheck": "Vérification de la liste noire DNS"
"blacklisted": "L'adresse IP de ce serveur {{ ip }} est sur liste noire."
},
"dnsStatus": {
"recordNotSet": "non défini",
@@ -1240,13 +972,7 @@
},
"config": {
"title": "Configuration de la messagerie {{ domain }}",
"clientConfiguration": "Configuration des clients de messagerie",
"sending": {
"title": "Envoi"
},
"receiving": {
"title": "Réception"
}
"clientConfiguration": "Configuration des clients de messagerie"
},
"editMailinglistDialog": {
"title": "Modifier la liste de diffusion {{ name }}@{{ domain }}"
@@ -1258,11 +984,7 @@
"enablePop3": "Activer l'accès POP3",
"activeCheckbox": "L'adresse de messagerie est active"
},
"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"
}
"howToConnectInfoModal": "Configuration des clients de messagerie"
},
"domains": {
"syncDns": {
@@ -1318,24 +1040,12 @@
"bunnyAccessKey": "Bunny Access Key",
"ovhConsumerKey": "Consumer Key",
"ovhAppKey": "Application Key",
"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"
"ovhAppSecret": "Application Secret"
},
"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",
"confirmMessage": "Cela invalidera toutes les clés d'accès des utilisateurs.",
"confirmTitle": "Voulez-vous vraiment modifier le domaine du tableau de bord?"
"title": "Changer le domaine du tableau de bord"
},
"removeDialog": {
"removeAction": "Supprimer",
@@ -1348,14 +1058,7 @@
},
"provider": "Fournisseur",
"domain": "Domaine",
"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"
}
"title": "Domaines et Certificats"
},
"branding": {
"footer": {
@@ -1363,8 +1066,7 @@
},
"title": "Affichage",
"cloudronName": "Nom du Cloudron",
"logo": "Logo",
"backgroundImage": "Arrière-plan de la page de connexion"
"logo": "Logo"
},
"passwordResetEmail": {
"subject": "Réinitialisation du mot de passe [<%= cloudron %>]",
@@ -1413,8 +1115,7 @@
"new": "Nouveau",
"uploadFolder": "Charger un dossier",
"openTerminal": "Ouvrir le terminal",
"openLogs": "Afficher les journaux",
"refresh": "Actualiser"
"openLogs": "Afficher les journaux"
},
"renameDialog": {
"reallyOverwrite": "Un fichier portant ce nom existe déjà. Écraser le fichier existant?",
@@ -1508,9 +1209,7 @@
"downloadAction": "Télécharger",
"scheduler": "Planificateur/Cron",
"download": {
"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."
"download": "Télécharger"
},
"title": "Terminal"
},
@@ -1536,19 +1235,10 @@
"product": "Produit",
"memory": "Mémoire",
"uptime": "Durée de fonctionnement",
"activationTime": "Heure de création de Cloudron",
"cloudronVersion": "Version de Cloudron",
"ubuntuVersion": "Version de Ubuntu"
"activationTime": "Heure de création de Cloudron"
},
"graphs": {
"title": "Graphiques"
},
"locale": {
"title": "Paramètres régionaux"
},
"title": "Système",
"settings": {
"title": "Paramètres"
}
},
"services": {
@@ -1584,8 +1274,7 @@
"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",
@@ -1594,11 +1283,7 @@
"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",
"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"
"errorInternal": "Erreur interne, réessayer ultérieurement"
},
"newLoginEmail": {
"salutation": "Bonjour <%= user %>,",
@@ -1618,8 +1303,7 @@
"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)",
"loginRedirectUriPlaceholder": "URL séparées par des virgules"
"loginRedirectUri": "Url de retour post connexion (séparées par des virgules s'il y en a plus d'une)"
},
"description": "Cloudron peut agir en tant que fournisseur OpenID Connect pour les applications internes et les services externes.",
"deleteClientDialog": {
@@ -1635,73 +1319,6 @@
},
"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."
}
}
+1 -2
View File
@@ -1207,8 +1207,7 @@
"saveAction": "Simpan",
"aliases": "Alias",
"addAliasAction": "Tambahkan alias",
"noAliases": "Tidak ada domain alias",
"overwriteDns": "Menimpa catatan DNS yang ada pada {domains}"
"noAliases": "Tidak ada domain alias"
},
"accessControl": {
"userManagement": {
+1 -2
View File
@@ -785,8 +785,7 @@
"noRedirections": "Geen domein-omleidingen",
"noAliases": "Geen alias-domeinen",
"addAliasAction": "Alias toevoegen",
"aliases": "Aliassen",
"overwriteDns": "Overschrijf bestaande DNS records van {domains}"
"aliases": "Aliassen"
},
"accessControl": {
"userManagement": {
+1 -3
View File
@@ -16,7 +16,6 @@ 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('');
@@ -123,7 +122,6 @@ async function onRevokeToken(apiToken) {
onMounted(async () => {
await refreshApiTokens();
loading.value = false;
});
</script>
@@ -186,7 +184,7 @@ onMounted(async () => {
<div v-html="$t('profile.apiTokens.description', { apiDocsLink: 'https://docs.cloudron.io/api.html' })"></div>
<br/>
<TableView :columns="columns" :model="apiTokens" :busy="loading" :placeholder="$t('profile.apiTokens.noTokensPlaceholder')">
<TableView :columns="columns" :model="apiTokens" :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>
+2 -4
View File
@@ -65,7 +65,6 @@ 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 = {};
@@ -164,7 +163,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.oidc || app.manifest.addons.proxyAuth);
const sso = app.sso && (app.manifest.addons.ldap || app.manifest.addons.proxyAuth);
if (!ftp && !sso) return;
@@ -180,7 +179,6 @@ onMounted(async () => {
});
await refresh();
loading.value = false;
});
</script>
@@ -244,7 +242,7 @@ onMounted(async () => {
<div>{{ $t('profile.appPasswords.description') }}</div>
<br/>
<TableView :columns="columns" :model="passwords" :busy="loading" :placeholder="$t('profile.appPasswords.noPasswordsPlaceholder')">
<TableView :columns="columns" :model="passwords" :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>
@@ -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="warning-label" v-html="$t('backups.configureBackupStorage.mountPointDescription', { providerDocsLink: 'https://docs.cloudron.io/backups/#user-managed-mount-point' })"></small>
<small class="helper-text" v-html="$t('backups.configureBackupStorage.mountPointDescription', { providerDocsLink: `https://docs.cloudron.io/backups/#${provider}` })"></small>
</FormGroup>
<!-- CIFS/NFS/SSHFS -->
@@ -56,7 +56,6 @@ function needsPort80(dnsProvider, tlsProvider) {
function resetFields() {
dnsConfig.value.accessKeyId = '';
dnsConfig.value.accessKey = '';
dnsConfig.value.apiUrl = '';
dnsConfig.value.accessToken = '';
dnsConfig.value.apiKey = '';
dnsConfig.value.appKey = '';
@@ -135,16 +134,6 @@ function onGcdnsFileInputChange(event) {
<div class="warning-label" v-show="provider === 'manual'" v-html="$t('domains.domainDialog.manualInfo')"></div>
<div class="warning-label" v-show="needsPort80(provider, tlsProvider)" v-html="$t('domains.domainDialog.letsEncryptInfo')"></div>
<!-- powerdns -->
<FormGroup v-if="provider === 'powerdns'">
<label for="powerdnsApiUrlInput">{{ $t('domains.domainDialog.powerdnsApiUrl') }}</label>
<TextInput id="powerdnsApiUrlInput" type="url" v-model="dnsConfig.apiUrl" placeholder="https://ns1.example.com:8081" required />
</FormGroup>
<FormGroup v-if="provider === 'powerdns'">
<label for="powerdnsApiKeyInput">{{ $t('domains.domainDialog.powerdnsApiKey') }}</label>
<MaskedInput id="powerdnsApiKeyInput" v-model="dnsConfig.apiKey" required />
</FormGroup>
<!-- Route53 -->
<FormGroup v-if="provider === 'route53'">
<label for="accessKeyIdInput">{{ $t('domains.domainDialog.route53AccessKeyId') }}</label>
+15 -13
View File
@@ -1,7 +1,7 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { Dialog, TextInput, ClipboardButton, FormGroup, InputGroup } from '@cloudron/pankow';
import { Dialog, TextInput, ClipboardButton, FormGroup, Button, InputGroup } from '@cloudron/pankow';
import UsersModel from '../models/UsersModel.js';
const usersModel = UsersModel.create();
@@ -13,16 +13,17 @@ const password = ref('');
const success = ref(false);
const busy = ref(false);
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);
// 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));
}
return blocks.join('-');
password.value = tmp;
}
async function onSubmit() {
@@ -44,7 +45,7 @@ defineExpose({
u = JSON.parse(JSON.stringify(u)); // make a copy
user.value = u;
success.value = false;
password.value = generatePassword();
password.value = '';
formError.value = '';
dialog.value.open();
@@ -70,8 +71,9 @@ defineExpose({
<FormGroup>
<label for="passwordInput">{{ $t('users.setGhostDialog.password') }}</label>
<InputGroup>
<TextInput id="passwordInput" v-model="password" readonly style="flex-grow: 1;"/>
<ClipboardButton :value="password" />
<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" />
</InputGroup>
</FormGroup>
</fieldset>
+1 -1
View File
@@ -209,7 +209,7 @@ body {
color: white;
font-family: monospace;
font-size: 14px;
white-space: pre-wrap;
white-space: nowrap;
width: 100%;
}
+5 -5
View File
@@ -113,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" :disabled="memoryLimitBusy || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || !!app.taskId" />
<input type="range" id="memoryLimitInput" v-model="memoryLimit" step="134217728" :min="memoryTicks[0]" :max="memoryTicks[memoryTicks.length-1]" list="memoryLimitTicks" />
<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" :disabled="cpuQuotaBusy || (app.error && app.error.installationState !== ISTATES.PENDING_RESIZE) || !!app.taskId" />
<input type="range" id="cpuQuotaInput" v-model="cpuQuota" step="1" min="1" max="100" list="cpuQuotaTicks" />
<datalist id="cpuQuotaTicks">
<option value="25"></option>
<option value="50"></option>
@@ -134,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 && 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>
+1 -1
View File
@@ -296,6 +296,7 @@ const STORAGE_PROVIDERS = [
{ 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' },
@@ -312,7 +313,6 @@ 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 = [
-4
View File
@@ -23,7 +23,6 @@ const providers = [
{ name: 'Porkbun', value: 'porkbun' },
{ name: 'Vultr', value: 'vultr' },
{ name: 'Wildcard', value: 'wildcard' },
{ name: 'PowerDNS', value: 'powerdns' },
{ name: 'Manual (not recommended)', value: 'manual' },
{ name: 'No-op (only for development)', value: 'noop' }
];
@@ -91,9 +90,6 @@ function filterConfigForProvider(provider, config) {
case 'porkbun':
props = ['apikey', 'secretapikey'];
break;
case 'powerdns':
props = ['apiUrl', 'apiKey'];
break;
}
const ret = {
+1 -2
View File
@@ -82,8 +82,7 @@ export function create(type, id, options = {}) {
}
const time = data.realtimeTimestamp ? moment(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss') : '';
const escaped = ansiToHtml(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message)));
const html = escaped.replace(/\n/g, '<br>');
const html = ansiToHtml(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message)));
eventSource._lastMessage = { time, html };
lineHandler(time, html);
+1 -1
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) {
+1 -4
View File
@@ -100,10 +100,7 @@ async function onRemoveSite(site) {
if (!yes) return;
const [error] = await backupSitesModels.del(site.id);
if (error) {
window.pankow.notify({ text: error.body?.message || 'Failed to delete backup site', type: 'danger' });
return console.error(error);
}
if (error) console.error(error);
await refresh();
+3 -1
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 } from '@cloudron/pankow';
import { Button, TableView, TextInput, InputDialog, ProgressBar } from '@cloudron/pankow';
import Certificates from '../components/Certificates.vue';
import ActionBar from '../components/ActionBar.vue';
import SyncDns from '../components/SyncDns.vue';
@@ -150,6 +150,8 @@ 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) }}
+3 -1
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 } from '@cloudron/pankow';
import { Button, TableView, InputDialog, TextInput, ProgressBar } from '@cloudron/pankow';
import ActionBar from '../components/ActionBar.vue';
import Section from '../components/Section.vue';
import GroupDialog from '../components/GroupDialog.vue';
@@ -163,6 +163,8 @@ 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>
+3 -1
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 } from '@cloudron/pankow';
import { Button, TextInput, SingleSelect, TableView, InputDialog, ProgressBar } from '@cloudron/pankow';
import { ROLES } from '../constants.js';
import Section from '../components/Section.vue';
import ActionBar from '../components/ActionBar.vue';
@@ -293,6 +293,8 @@ 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"/>
-1
View File
@@ -267,7 +267,6 @@ 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'">
+1 -6
View File
@@ -34,7 +34,6 @@ 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,
@@ -82,16 +81,14 @@ 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
@@ -105,8 +102,6 @@ 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,
+8
View File
@@ -364,6 +364,7 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1000.0.tgz",
"integrity": "sha512-7kPy33qNGq3NfwHC0412T6LDK1bp4+eiPzetX0sVd9cpTSXuQDKpoOFnB0Njj6uZjJDcLS3n2OeyarwwgkQ0Ow==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@aws-crypto/sha1-browser": "5.2.0",
"@aws-crypto/sha256-browser": "5.2.0",
@@ -1228,6 +1229,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -1266,6 +1268,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3130,6 +3133,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -3218,6 +3222,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3374,6 +3379,7 @@
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"bare-abort-controller": "*"
},
@@ -4567,6 +4573,7 @@
"integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
@@ -6190,6 +6197,7 @@
"resolved": "https://registry.npmjs.org/koa/-/koa-3.1.1.tgz",
"integrity": "sha512-KDDuvpfqSK0ZKEO2gCPedNjl5wYpfj+HNiuVRlbhd1A88S3M0ySkdf2V/EJ4NWt5dwh5PXCdcenrKK2IQJAxsg==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^1.3.8",
"content-disposition": "~0.5.4",
+1 -4
View File
@@ -56,10 +56,7 @@ 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)
echo "==> Update tldjs rules"
(cd "${bundle_dir}" && node node_modules/tldjs/bin/update.js)
(cd "${bundle_dir}" && npm ci --omit=dev --omit=optional --tldjs-update-rules)
echo "==> Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
+2 -11
View File
@@ -2,22 +2,13 @@ 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 '@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');
@@ -50,7 +41,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 = generatePassword();
const password = hat(16 * 4);
const hashedPassword = crypto.createHash('sha256').update(password).digest('base64');
const appPassword = {
+1 -2
View File
@@ -1377,8 +1377,7 @@ async function appendLogLine(app, line) {
const logFilePath = path.join(paths.LOG_DIR, app.id, 'app.log');
const isoDate = new Date(new Date().toUTCString()).toISOString();
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}`);
if (!safe.fs.appendFileSync(logFilePath, `${isoDate} ${line}\n`)) console.error(`Could not append log line for app ${app.id} at ${logFilePath}: ${safe.error.message}`);
}
async function checkManifest(manifest) {
-11
View File
@@ -81,22 +81,11 @@ 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,
+21 -18
View File
@@ -9,43 +9,47 @@ import middleware from './middleware/index.js';
import safe from '@cloudron/safetydance';
import users from './users.js';
import util from 'node:util';
import { HttpError, HttpSuccess } from '@cloudron/connect-lastmile';
import { HttpError } from '@cloudron/connect-lastmile';
const { trace, log } = logger('authserver');
let gHttpServer = null;
async function verifyCredentials(req, res, next) {
// Internal loopback-only password check; skipTotpCheck matches LDAP bind behavior for automated callers.
const VERIFY_OPTIONS = { skipTotpCheck: true };
function peerIpForAppLookup(req) {
let ip = req.socket.remoteAddress;
if (!ip) return '';
if (ip.startsWith('::ffff:')) ip = ip.slice(7);
return ip;
}
async function verifyPost(req, res, next) {
if (!req.body || typeof req.body !== 'object') return next(new HttpError(400, 'Body must be a JSON object'));
const { identifier, password } = req.body;
if (typeof identifier !== 'string' || identifier.length === 0) return next(new HttpError(400, 'identifier must be a non-empty string'));
if (typeof password !== 'string' || password.length === 0) return next(new HttpError(400, 'password must be a non-empty string'));
trace(`verifyCredentials: attempt for ${identifier}`);
trace(`verifyPost: attempt for ${identifier}`);
let verifyFunc;
if (identifier.startsWith('uid-')) verifyFunc = users.verifyWithId;
else if (identifier.includes('@')) verifyFunc = users.verifyWithEmail;
else verifyFunc = users.verifyWithUsername;
const [, connectingApp] = await safe(apps.getByIpAddress(req.socket.remoteAddress));
const [, connectingApp] = await safe(apps.getByIpAddress(peerIpForAppLookup(req)));
const appId = connectingApp ? connectingApp.id : '';
// Internal loopback-only password check; skipTotpCheck matches LDAP bind behavior for automated callers.
const [verifyError, user] = await safe(verifyFunc(identifier, password, appId, { skipTotpCheck: true }));
const [verifyError, user] = await safe(verifyFunc(identifier, password, appId, VERIFY_OPTIONS));
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, verifyError.message));
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Username and password does not match'));
if (verifyError) return next(new HttpError(500, verifyError));
if (!user) return next(new HttpError(401, 'Username and password does not match'));
next(new HttpSuccess(200, {
id: user.id,
username: user.username,
email: user.email,
displayName: user.displayName
}));
res.status(200).json({ ok: true });
}
async function start() {
@@ -56,16 +60,13 @@ async function start() {
const json = express.json({ strict: true, limit: '2mb' });
app.post('/verify-credentials', json, verifyCredentials);
app.post('/verify-credentials', json, verifyPost);
app.use(middleware.lastMile());
gHttpServer = http.createServer(app);
// In production the auth HTTP API is only reachable on the docker bridge IP. Tests run on the
// host and connect via 127.0.0.1, and app IP checks rely on that remote address.
const bindHost = constants.TEST ? '127.0.0.1' : constants.DOCKER_IPv4_GATEWAY;
log(`start: listening on ${bindHost}:${constants.AUTH_PORT}`);
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.AUTH_PORT, bindHost);
log(`start: listening on ${constants.DOCKER_IPv4_GATEWAY}:${constants.AUTH_PORT}`);
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.AUTH_PORT, constants.DOCKER_IPv4_GATEWAY);
}
async function stop() {
@@ -79,3 +80,5 @@ export default {
start,
stop,
};
export { peerIpForAppLookup };
-2
View File
@@ -306,8 +306,6 @@ async function del(backupSite, auditSource) {
assert.strictEqual(typeof backupSite, 'object');
assert.strictEqual(typeof auditSource, 'object');
if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
await safe(storageApi(backupSite).teardown(backupSite.config), { debug: log }); // ignore error
const queries = [
+2 -3
View File
@@ -34,7 +34,6 @@ import dnsNoop from './dns/noop.js';
import dnsManual from './dns/manual.js';
import dnsOvh from './dns/ovh.js';
import dnsPorkbun from './dns/porkbun.js';
import dnsPowerdns from './dns/powerdns.js';
import dnsWildcard from './dns/wildcard.js';
const { log } = logger('dns');
@@ -46,7 +45,7 @@ const DNS_PROVIDERS = {
godaddy: dnsGodaddy, inwx: dnsInwx, linode: dnsLinode, vultr: dnsVultr,
namecom: dnsNamecom, namecheap: dnsNamecheap, netcup: dnsNetcup, hetzner: dnsHetzner,
hetznercloud: dnsHetznercloud, noop: dnsNoop, manual: dnsManual, ovh: dnsOvh,
porkbun: dnsPorkbun, powerdns: dnsPowerdns, wildcard: dnsWildcard
porkbun: dnsPorkbun, wildcard: dnsWildcard
};
// choose which subdomain backend we use for test purpose we use route53
@@ -234,7 +233,7 @@ async function registerLocation(location, options, recordType, recordValue) {
if (upsertError) {
const retryable = upsertError.reason === BoxError.BUSY || upsertError.reason === BoxError.EXTERNAL_ERROR;
log(`registerLocation: Upsert error. retryable: ${retryable}. ${upsertError.message}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, `${location.domain}: ${upsertError.message}`, { domain: location, retryable });
throw new BoxError(BoxError.EXTERNAL_ERROR, `${location.domain}: ${getError.message}`, { domain: location, retryable });
}
}
-165
View File
@@ -1,165 +0,0 @@
import assert from 'node:assert';
import BoxError from '../boxerror.js';
import logger from '../logger.js';
import dns from '../dns.js';
import safe from '@cloudron/safetydance';
import superagent from '@cloudron/superagent';
import waitForDns from './waitfordns.js';
const { log } = logger('dns/powerdns');
function formatError(response) {
return `PowerDNS error ${response.status} ${response.body ? JSON.stringify(response.body) : response.text}`;
}
function removePrivateFields(domainObject) {
delete domainObject.config.apiKey;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (!Object.hasOwn(newConfig, 'apiKey')) newConfig.apiKey = currentConfig.apiKey;
}
async function get(domainObject, subdomain, type) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
const domainConfig = domainObject.config;
const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
const zoneName = domainObject.zoneName + '.';
const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
log(`get: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type}`);
const [error, response] = await safe(superagent.get(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
.set('X-API-Key', domainConfig.apiKey)
.timeout(30 * 1000)
.retry(5)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, formatError(response));
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
const rrset = response.body.rrsets.find(r => r.name === fqdn && r.type === type);
if (!rrset) return [];
return rrset.records.map(r => {
if (type === 'TXT') return r.content.replace(/^"(.*)"$/, '$1');
return r.content;
});
}
async function upsert(domainObject, subdomain, type, values) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
const domainConfig = domainObject.config;
const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
const zoneName = domainObject.zoneName + '.';
const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
log(`upsert: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type} values ${values}`);
const records = values.map(v => {
let content = v;
if (type === 'TXT' && !content.startsWith('"')) content = `"${v}"`;
return { content, disabled: false };
});
const rrset = {
name: fqdn,
type: type,
ttl: 60,
changetype: 'REPLACE',
records: records
};
const [error, response] = await safe(superagent.patch(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
.set('X-API-Key', domainConfig.apiKey)
.send({ rrsets: [rrset] })
.timeout(30 * 1000)
.retry(5)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
}
async function del(domainObject, subdomain, type, values) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
const domainConfig = domainObject.config;
const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
const zoneName = domainObject.zoneName + '.';
const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
log(`del: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type} values ${values}`);
const rrset = {
name: fqdn,
type: type,
changetype: 'DELETE'
};
const [error, response] = await safe(superagent.patch(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
.set('X-API-Key', domainConfig.apiKey)
.send({ rrsets: [rrset] })
.timeout(30 * 1000)
.retry(5)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
if (response.status === 404 || response.status === 422) return;
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
}
async function wait(domainObject, subdomain, type, value, options) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object');
const fqdn = dns.fqdn(subdomain, domainObject.domain);
await waitForDns(fqdn, domainObject.zoneName, type, value, options);
}
async function verifyDomainConfig(domainObject) {
assert.strictEqual(typeof domainObject, 'object');
const domainConfig = domainObject.config;
if (!domainConfig.apiUrl || typeof domainConfig.apiUrl !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'apiUrl must be a non-empty string');
if (!domainConfig.apiKey || typeof domainConfig.apiKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'apiKey must be a non-empty string');
const testSubdomain = 'cloudrontestdns';
const testIp = '127.0.0.1';
await upsert(domainObject, testSubdomain, 'A', [testIp]);
await del(domainObject, testSubdomain, 'A', [testIp]);
return {
apiUrl: domainConfig.apiUrl,
apiKey: domainConfig.apiKey
};
}
export default {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
+6 -13
View File
@@ -327,10 +327,7 @@ async function startContainer(containerId) {
const container = gConnection.getContainer(containerId);
// attempt a few times in case ephemeral ports got allocated during container recreation
const [error] = await safe(retry({ times: 3, interval: 30000, log, retry: (err) => err.statusCode === 500 && /address already in use/.test(err.message) }, async function () {
return await container.start();
}));
const [error] = await safe(container.start());
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Container ${containerId} not found`);
if (error && error.statusCode === 400) throw new BoxError(BoxError.BAD_FIELD, error); // e.g start.sh is not executable
if (error && error.statusCode !== 304) throw new BoxError(BoxError.DOCKER_ERROR, error); // 304 means already started
@@ -608,7 +605,7 @@ async function createSubcontainer(app, name, cmd, options) {
const stdEnv = [
'LANG=C.UTF-8',
'CLOUDRON=1',
`CLOUDRON_PROXY_IP=${constants.DOCKER_IPv4_GATEWAY}`,
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${app.id}`,
`CLOUDRON_WEBADMIN_ORIGIN=https://${dashboardFqdn}`,
`CLOUDRON_API_ORIGIN=https://${dashboardFqdn}`,
@@ -716,15 +713,11 @@ async function createSubcontainer(app, name, cmd, options) {
containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network
// Do not inject for AdGuard. It ends up resolving the dashboard domain as the docker bridge IP
if (manifest.id !== 'com.adguard.home.cloudronapp') {
containerOptions.HostConfig.ExtraHosts.push(`${dashboardFqdn}:${constants.DOCKER_IPv4_GATEWAY}`);
if (manifest.id !== 'com.adguard.home.cloudronapp') containerOptions.HostConfig.ExtraHosts.push(`${dashboardFqdn}:172.18.0.1`);
if (manifest.addons?.sendmail?.requiresValidCertificate) {
const { fqdn:mailFqdn } = await mailServer.getLocation();
// When mail fqdn and dashboard fqdn are the same, they must resolve to the same IP (currently bridge IP)
// if they are not the same, the requests can go to any of the multiple IPs
if (dashboardFqdn !== mailFqdn) containerOptions.HostConfig.ExtraHosts.push(`${mailFqdn}:${constants.DOCKER_IPv4_GATEWAY}`);
}
if (manifest.addons?.sendmail?.requiresValidCertificate) {
const { fqdn:mailFqdn } = await mailServer.getLocation();
containerOptions.HostConfig.ExtraHosts.push(`${mailFqdn}:${constants.MAIL_SERVICE_IPv4}`);
}
containerOptions.NetworkingConfig = {
+1 -1
View File
@@ -177,7 +177,7 @@ async function start() {
});
});
log(`start: listening on ${constants.DOCKER_IPv4_GATEWAY}:${constants.DOCKER_PROXY_PORT}`);
log(`start: listening on 172.18.0.1:${constants.DOCKER_PROXY_PORT}`);
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.DOCKER_PROXY_PORT, constants.DOCKER_IPv4_GATEWAY);
}
+1 -1
View File
@@ -4,7 +4,7 @@
export default {
// a version change recreates all containers with latest docker config
'version': '49.10.0',
'version': '49.9.0',
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256 . note this has registry in it because manifest id is registry specific!
+1 -2
View File
@@ -5,8 +5,7 @@ const LOG_ENABLED = process.env.BOX_ENV !== 'test' || !!process.env.LOG;
function output(namespace, args) {
const ts = new Date().toISOString();
const msg = util.format(...args).replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
process.stdout.write(`${ts} ${namespace}: ${msg}\n`);
process.stdout.write(`${ts} ${namespace}: ${util.format(...args)}\n`);
}
export default function logger(namespace) {
+1 -4
View File
@@ -34,12 +34,9 @@ class LogStream extends TransformStream {
message = line.slice(data[0].length+1);
}
// unescape \\n → newline and \\\\ → backslash (writers escape newlines to keep one entry per line)
message = (message || line).replace(/\\(\\|n)/g, (_, c) => c === 'n' ? '\n' : '\\');
return JSON.stringify({
realtimeTimestamp: timestamp * 1000, // timestamp info can be missing (0) for app logs via logPaths
message: message,
message: message || line, // send the line if message parsing failed
source: this._options.source
}) + '\n';
}
+2 -2
View File
@@ -130,7 +130,7 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) {
const allowInbound = await createMailConfig(mailFqdn);
const ports = allowInbound ? `-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587 -p ${constants.DOCKER_IPv4_GATEWAY}:2587:2587 -p 465:2465 -p 995:9995` : '';
const ports = allowInbound ? '-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587 -p 465:2465 -p 995:9995' : '';
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
const logLevel = serviceConfig.recoveryMode ? 'data' : 'info';
@@ -144,7 +144,7 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) {
--log-opt tag=mail \
-m ${memoryLimit} \
--memory-swap -1 \
--dns ${constants.DOCKER_IPv4_GATEWAY} \
--dns 172.18.0.1 \
--dns-search=. \
--ip ${constants.MAIL_SERVICE_IPv4} \
-e CLOUDRON_MAIL_TOKEN=${cloudronToken} \
+6
View File
@@ -107,8 +107,14 @@ server {
# https://github.com/twitter/secureheaders
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
add_header X-XSS-Protection "1; mode=block";
proxy_hide_header X-XSS-Protection;
add_header X-Download-Options "noopen";
proxy_hide_header X-Download-Options;
add_header X-Content-Type-Options "nosniff";
proxy_hide_header X-Content-Type-Options;
add_header X-Permitted-Cross-Domain-Policies "none";
proxy_hide_header X-Permitted-Cross-Domain-Policies;
# See header handling from upstream on top of this file
add_header Referrer-Policy $hrp;
+5 -6
View File
@@ -228,7 +228,6 @@ async function stopUnusedServices() {
const usedAddons = new Set();
for (const app of allApps) {
for (const addon of Object.keys(app.manifest.addons || {})) {
if (addon === 'turn' && app.manifest.addons.turn?.optional && !app.enableTurn) continue;
usedAddons.add(addon);
}
}
@@ -565,10 +564,10 @@ async function setupLdap(app, options) {
if (!app.sso) return;
const env = [
{ name: 'CLOUDRON_LDAP_SERVER', value: constants.DOCKER_IPv4_GATEWAY },
{ name: 'CLOUDRON_LDAP_HOST', value: constants.DOCKER_IPv4_GATEWAY }, // to keep things in sync with the database _HOST vars
{ name: 'CLOUDRON_LDAP_SERVER', value: '172.18.0.1' },
{ name: 'CLOUDRON_LDAP_HOST', value: '172.18.0.1' }, // to keep things in sync with the database _HOST vars
{ name: 'CLOUDRON_LDAP_PORT', value: '' + constants.LDAP_PORT },
{ name: 'CLOUDRON_LDAP_URL', value: 'ldap://' + constants.DOCKER_IPv4_GATEWAY + ':' + constants.LDAP_PORT },
{ name: 'CLOUDRON_LDAP_URL', value: 'ldap://172.18.0.1:' + constants.LDAP_PORT },
{ name: 'CLOUDRON_LDAP_USERS_BASE_DN', value: 'ou=users,dc=cloudron' },
{ name: 'CLOUDRON_LDAP_GROUPS_BASE_DN', value: 'ou=groups,dc=cloudron' },
{ name: 'CLOUDRON_LDAP_BIND_DN', value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
@@ -705,7 +704,7 @@ async function startMysql(existingInfra) {
--log-opt tag=mysql \
--ip ${constants.MYSQL_SERVICE_IPv4} \
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
-e CLOUDRON_MYSQL_ROOT_HOST=${constants.DOCKER_IPv4_GATEWAY} \
-e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \
-e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \
-v ${dataDir}/mysql:/var/lib/mysql \
--label isCloudronManaged=true \
@@ -1278,7 +1277,7 @@ async function setupDocker(app, options) {
log('Setting up docker');
const env = [ { name: 'CLOUDRON_DOCKER_HOST', value: `tcp://${constants.DOCKER_IPv4_GATEWAY}:${constants.DOCKER_PROXY_PORT}` } ];
const env = [ { name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` } ];
await addonConfigs.set(app.id, 'docker', env);
}
+1 -1
View File
@@ -79,7 +79,7 @@ function spawn(tag, file, args, options) {
e.timedOut = timedOut;
e.terminated = terminated;
log(`${tag}: ${file} ${args.join(' ').replace(/\n/g, '\\n')} errored with code ${e.code} and signal ${e.signal} timeout ${e.timedOut} terminated ${e.terminated} - stdout: "${e.stdoutString}" - stderr: "${e.stderrString}"`);
log(`${tag}: ${file} ${args.join(' ').replace(/\n/g, '\\n')} errored`, e);
reject(e);
});
+5 -5
View File
@@ -201,9 +201,9 @@ async function copyInternal(config, fromPath, toPath, options, progressCallback)
safe.fs.unlinkSync(identityFilePath);
if (!safe.fs.writeFileSync(identityFilePath, `${config.mountOptions.privateKey}\n`, { mode: 0o600 })) throw new BoxError(BoxError.FS_ERROR, `Could not write temporary private key: ${safe.error.message}`);
const sshOptions = [ '-o', 'StrictHostKeyChecking no', '-i', identityFilePath, '-p', config.mountOptions.port, `${config.mountOptions.user}@${config.mountOptions.host}` ];
const sshArgs = sshOptions.concat([ 'cp', cpOptions, path.join(config.mountOptions.remoteDir, config.prefix ?? '', fromPath), path.join(config.mountOptions.remoteDir, config.prefix ?? '', toPath) ]);
const [remoteCopyError] = await safe(shell.spawn('ssh', sshArgs, {}));
const sshOptions = [ '-o', '"StrictHostKeyChecking no"', '-i', identityFilePath, '-p', config.mountOptions.port, `${config.mountOptions.user}@${config.mountOptions.host}` ];
const sshArgs = sshOptions.concat([ 'cp', cpOptions, path.join(config.prefix ?? '', fromPath), path.join(config.prefix ?? '', toPath) ]);
const [remoteCopyError] = await safe(shell.spawn('ssh', sshArgs, { shell: true }));
safe.fs.unlinkSync(identityFilePath);
if (!remoteCopyError) return;
if (remoteCopyError.code === 255) throw new BoxError(BoxError.EXTERNAL_ERROR, `SSH connection error: ${remoteCopyError.message}`); // do not attempt fallback copy for ssh errors
@@ -264,9 +264,9 @@ async function removeDir(config, limits, remotePathPrefix, progressCallback) {
safe.fs.unlinkSync(identityFilePath);
if (!safe.fs.writeFileSync(identityFilePath, `${config.mountOptions.privateKey}\n`, { mode: 0o600 })) throw new BoxError(BoxError.FS_ERROR, `Could not write temporary private key: ${safe.error.message}`);
const sshOptions = [ '-o', 'StrictHostKeyChecking no', '-i', identityFilePath, '-p', config.mountOptions.port, `${config.mountOptions.user}@${config.mountOptions.host}` ];
const sshOptions = [ '-o', '"StrictHostKeyChecking no"', '-i', identityFilePath, '-p', config.mountOptions.port, `${config.mountOptions.user}@${config.mountOptions.host}` ];
const sshArgs = sshOptions.concat([ 'rm', '-rf', path.join(config.prefix ?? '', remotePathPrefix) ]);
const [remoteRmError] = await safe(shell.spawn('ssh', sshArgs, {}));
const [remoteRmError] = await safe(shell.spawn('ssh', sshArgs, { shell: true }));
safe.fs.unlinkSync(identityFilePath);
if (!remoteRmError) return;
if (remoteRmError.code === 255) throw new BoxError(BoxError.EXTERNAL_ERROR, `SSH connection error: ${remoteRmError.message}`); // do not attempt fallback copy for ssh errors
+2 -6
View File
@@ -70,12 +70,8 @@ async function setupNetworking() {
// taskworker.sh forwards the exit code of the actual worker. It's either a raw signal number OR the exit code. So, choose exit codes > 31
// 50 - internal error , 70 - SIGTERM exit
function exitSync(status) {
const ts = (new Date()).toISOString();
if (status.error) {
const escapedStack = status.error.stack.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
fs.write(logFd, `${ts} ${escapedStack}\n`, function () {});
}
fs.write(logFd, `${ts} Exiting with code ${status.code}\n`, function () {});
if (status.error) fs.write(logFd, status.error.stack + '\n', function () {});
fs.write(logFd, `${(new Date()).toISOString()} Exiting with code ${status.code}\n`, function () {});
fs.fsyncSync(logFd);
fs.closeSync(logFd);
process.exit(status.code);
+20 -10
View File
@@ -1,7 +1,7 @@
import { describe, it, before, after } from 'mocha';
import appPasswords from '../apppasswords.js';
import apps from '../apps.js';
import authServer from '../authserver.js';
import authServer, { peerIpForAppLookup } from '../authserver.js';
import constants from '../constants.js';
import common from './common.js';
import assert from 'node:assert/strict';
@@ -9,12 +9,22 @@ import superagent from '@cloudron/superagent';
const authUrl = `http://127.0.0.1:${constants.AUTH_PORT}`;
function assertVerifyCredentialsProfile(body, expected) {
assert.equal(body.id, expected.id);
assert.equal(body.username, expected.username);
assert.equal(body.email, expected.email);
assert.equal(body.displayName, expected.displayName);
}
describe('authserver peerIpForAppLookup', function () {
it('strips ipv4-mapped ipv6 prefix', function () {
const req = { socket: { remoteAddress: '::ffff:172.18.16.1' } };
assert.equal(peerIpForAppLookup(req), '172.18.16.1');
});
it('passes through plain ipv4', function () {
const req = { socket: { remoteAddress: '127.0.0.1' } };
assert.equal(peerIpForAppLookup(req), '127.0.0.1');
});
it('returns empty string when remoteAddress is missing', function () {
const req = { socket: {} };
assert.equal(peerIpForAppLookup(req), '');
});
});
describe('authserver HTTP', function () {
const { setup, cleanup, admin, app } = common;
@@ -36,7 +46,7 @@ describe('authserver HTTP', function () {
.ok(() => true);
assert.equal(response.status, 200);
assertVerifyCredentialsProfile(response.body, admin);
assert.equal(response.body.ok, true);
});
it('returns 200 with app password when containerIp matches peer', async function () {
@@ -49,7 +59,7 @@ describe('authserver HTTP', function () {
await appPasswords.del(id);
assert.equal(response.status, 200);
assertVerifyCredentialsProfile(response.body, admin);
assert.equal(response.body.ok, true);
});
it('returns 401 for app password when containerIp does not match peer', async function () {
@@ -77,6 +87,6 @@ describe('authserver HTTP', function () {
await apps.update(app.id, { containerIp: '127.0.0.1' });
assert.equal(response.status, 200);
assertVerifyCredentialsProfile(response.body, admin);
assert.equal(response.body.ok, true);
});
});
-73
View File
@@ -412,79 +412,6 @@ describe('dns provider', function () {
const TOKEN = 'sometoken';
const NAMECOM_API = 'https://api.name.com/v4';
describe('powerdns', function () {
const API_URL = 'http://ns1.example.com:8081';
const API_KEY = 'secret';
before(async function () {
domainCopy.provider = 'powerdns';
domainCopy.config = {
apiUrl: API_URL,
apiKey: API_KEY
};
await domains.setConfig(domainCopy.domain, domainCopy, auditSource);
});
it('upsert non-existing record succeeds', async function () {
nock.cleanAll();
const zoneName = domainCopy.zoneName + '.';
const fqdn = 'test.' + domainCopy.domain + '.';
const req1 = nock(API_URL)
.patch('/api/v1/servers/localhost/zones/' + zoneName, body => {
return body.rrsets[0].name === fqdn &&
body.rrsets[0].type === 'A' &&
body.rrsets[0].changetype === 'REPLACE' &&
body.rrsets[0].records[0].content === '1.2.3.4';
})
.reply(204);
await dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4']);
assert.ok(req1.isDone());
});
it('get succeeds', async function () {
nock.cleanAll();
const zoneName = domainCopy.zoneName + '.';
const fqdn = 'test.' + domainCopy.domain + '.';
const req1 = nock(API_URL)
.get('/api/v1/servers/localhost/zones/' + zoneName)
.reply(200, {
rrsets: [{
name: fqdn,
type: 'A',
records: [{ content: '1.2.3.4', disabled: false }]
}]
});
const result = await dns.getDnsRecords('test', domainCopy.domain, 'A');
assert.deepEqual(result, ['1.2.3.4']);
assert.ok(req1.isDone());
});
it('del succeeds', async function () {
nock.cleanAll();
const zoneName = domainCopy.zoneName + '.';
const fqdn = 'test.' + domainCopy.domain + '.';
const req1 = nock(API_URL)
.patch('/api/v1/servers/localhost/zones/' + zoneName, body => {
return body.rrsets[0].name === fqdn &&
body.rrsets[0].type === 'A' &&
body.rrsets[0].changetype === 'DELETE';
})
.reply(204);
await dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4']);
assert.ok(req1.isDone());
});
});
before(async function () {
domainCopy.provider = 'namecom';
domainCopy.config = {
+1 -1
View File
@@ -8,7 +8,7 @@ import nock from 'nock';
import syslogServer from '../../syslog.js';
const DOCKER = `docker -H tcp://${constants.DOCKER_IPv4_GATEWAY}:${constants.DOCKER_PROXY_PORT} `;
const DOCKER = `docker -H tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT} `;
async function exec(cmd) {
return new Promise((resolve, reject) => {
+1 -2
View File
@@ -59,8 +59,7 @@ async function start() {
try {
fs.mkdirSync(appLogDir, { recursive: true });
const escaped = info.message.trim().replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
fs.appendFileSync(`${appLogDir}/app.log`, `${info.timestamp} ${escaped}\n`);
fs.appendFileSync(`${appLogDir}/app.log`, `${info.timestamp} ${info.message.trim()}\n`);
} catch (error) {
log(error);
}